Compare commits

..

12 Commits

Author SHA1 Message Date
dayuan.jiang
6086c4177a fix: improve duplicate model validation UX in config dialog
- Add inline error display for duplicate model IDs
- Show red border on input when error exists
- Validate on blur with shake animation for edit errors
- Prevent saving empty model names
- Clear errors when user starts typing
- Simplify error styling (small red text, no heavy chips)
2025-12-22 22:27:49 +09:00
dayuan.jiang
33fd2a16e6 fix: improve duplicate model ID notifications
- Add toast notification when trying to add duplicate model
- Allow free typing when editing model ID, validate on blur
- Show warning toast instead of blocking input
2025-12-22 21:57:02 +09:00
dayuan.jiang
41c450516c fix: prevent duplicate model IDs within same provider
- Block adding model if ID already exists in provider
- Block editing model ID to match existing model in provider
2025-12-22 21:53:56 +09:00
dayuan.jiang
0e8783ccfb fix: UI/UX improvements for model configuration dialog
- Add gradient header styling with icon badge
- Change Configuration section icon from Key to Settings2
- Add duplicate model detection with warning banner and inline removal
- Filter out already-added models from suggestions dropdown
- Add type-to-confirm for deleting providers with 3+ models
- Enhance delete confirmation dialog with warning icon
- Improve model selector discoverability (show model name + chevron)
- Add truncation for long model names with title tooltip
- Remove AI provider settings from Settings dialog (now in Model Config)
- Extract ValidationButton into reusable component
2025-12-22 21:49:29 +09:00
dayuan.jiang
7cf6d7e7bd chore: remove unused code
- Remove unused setAccessCodeRequired state in chat-panel.tsx
- Remove unused getSelectedModel export in model-config.ts
2025-12-22 20:46:12 +09:00
dayuan.jiang
7ed7b29274 fix: complete bedrock support and UI/UX improvements
- Add bedrock to ALLOWED_CLIENT_PROVIDERS for client credentials
- Pass AWS credentials through full chain (headers → API → provider)
- Replace non-existent GPT-5 models with real ones (o1, o3-mini)
- Add accessibility: aria-labels, focus-visible rings, inline errors
- Add more AWS regions (Ohio, London, Paris, Mumbai, Seoul, São Paulo)
- Fix setTimeout cleanup with useRef on component unmount
- Fix TypeScript type consistency in getSelectedAIConfig fallback
2025-12-22 20:40:12 +09:00
dayuan.jiang
1be0cfa06c fix: reset validation button to Test after success 2025-12-22 20:13:57 +09:00
dayuan.jiang
1f6ef7ac90 fix: reset Test button after validation completes 2025-12-22 20:11:46 +09:00
dayuan.jiang
56ca9d3f48 feat: add AWS credentials support for Bedrock provider
- Add AWS Access Key ID, Secret Access Key, Region fields for Bedrock
- Show different credential fields based on provider type
- Update validation API to handle Bedrock with AWS credentials
- Add region selector with common AWS regions
2025-12-22 20:09:16 +09:00
dayuan.jiang
e089702949 refactor: revert shadcn component changes, apply hover fix at usage site 2025-12-22 20:02:58 +09:00
dayuan.jiang
89b0a96b95 feat: improve model config UI and move selector to chat input
- Move model selector from header to chat input (left of send button)
- Add per-model validation status (queued, running, valid, invalid)
- Filter model selector to only show verified models
- Add editable model IDs in config dialog
- Add custom model input field alongside suggested models dropdown
- Fix hover states on provider buttons and select triggers
- Update OpenAI suggested models with GPT-5 series
- Add alert-dialog component for delete confirmation
2025-12-22 20:00:45 +09:00
dayuan.jiang
1e916aa86e feat: add multi-provider model configuration
- Add model config dialog for managing multiple AI providers
- Support for OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, DeepSeek, SiliconFlow, Ollama, and AI Gateway
- Add model selector dropdown in chat panel header
- Add API key validation endpoint
- Add custom model ID input with keyboard navigation
- Fix hover highlight in Command component
- Add suggested models for each provider including latest Claude 4.5 series
- Store configuration locally in browser
2025-12-22 17:58:05 +09:00
21 changed files with 294 additions and 551 deletions

View File

@@ -63,8 +63,6 @@ jobs:
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: |
NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=true
# Push to AWS ECR for App Runner auto-deploy # Push to AWS ECR for App Runner auto-deploy
- name: Configure AWS credentials - name: Configure AWS credentials

View File

@@ -26,10 +26,6 @@ ENV NEXT_TELEMETRY_DISABLED=1
ARG NEXT_PUBLIC_DRAWIO_BASE_URL=https://embed.diagrams.net ARG NEXT_PUBLIC_DRAWIO_BASE_URL=https://embed.diagrams.net
ENV NEXT_PUBLIC_DRAWIO_BASE_URL=${NEXT_PUBLIC_DRAWIO_BASE_URL} ENV NEXT_PUBLIC_DRAWIO_BASE_URL=${NEXT_PUBLIC_DRAWIO_BASE_URL}
# Build-time argument to show About link and Notice icon
ARG NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=false
ENV NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=${NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE}
# Build Next.js application (standalone mode) # Build Next.js application (standalone mode)
RUN npm run build RUN npm run build

View File

@@ -11,66 +11,6 @@ import { createOllama } from "ollama-ai-provider-v2"
export const runtime = "nodejs" export const runtime = "nodejs"
/**
* SECURITY: Check if URL points to private/internal network (SSRF protection)
* Blocks: localhost, private IPs, link-local, AWS metadata service
*/
function isPrivateUrl(urlString: string): boolean {
try {
const url = new URL(urlString)
const hostname = url.hostname.toLowerCase()
// Block localhost
if (
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname === "::1"
) {
return true
}
// Block AWS/cloud metadata endpoints
if (
hostname === "169.254.169.254" ||
hostname === "metadata.google.internal"
) {
return true
}
// Check for private IPv4 ranges
const ipv4Match = hostname.match(
/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,
)
if (ipv4Match) {
const [, a, b] = ipv4Match.map(Number)
// 10.0.0.0/8
if (a === 10) return true
// 172.16.0.0/12
if (a === 172 && b >= 16 && b <= 31) return true
// 192.168.0.0/16
if (a === 192 && b === 168) return true
// 169.254.0.0/16 (link-local)
if (a === 169 && b === 254) return true
// 127.0.0.0/8 (loopback)
if (a === 127) return true
}
// Block common internal hostnames
if (
hostname.endsWith(".local") ||
hostname.endsWith(".internal") ||
hostname.endsWith(".localhost")
) {
return true
}
return false
} catch {
// Invalid URL - block it
return true
}
}
interface ValidateRequest { interface ValidateRequest {
provider: string provider: string
apiKey: string apiKey: string
@@ -102,14 +42,6 @@ export async function POST(req: Request) {
) )
} }
// SECURITY: Block SSRF attacks via custom baseUrl
if (baseUrl && isPrivateUrl(baseUrl)) {
return NextResponse.json(
{ valid: false, error: "Invalid base URL" },
{ status: 400 },
)
}
// Validate credentials based on provider // Validate credentials based on provider
if (provider === "bedrock") { if (provider === "bedrock") {
if (!awsAccessKeyId || !awsSecretAccessKey || !awsRegion) { if (!awsAccessKeyId || !awsSecretAccessKey || !awsRegion) {

View File

@@ -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: getAssetUrl("/"), start_url: "/",
display: "standalone", display: "standalone",
background_color: "#f9fafb", background_color: "#f9fafb",
theme_color: "#171d26", theme_color: "#171d26",
icons: [ icons: [
{ {
src: getAssetUrl("/favicon-192x192.png"), src: "/favicon-192x192.png",
sizes: "192x192", sizes: "192x192",
type: "image/png", type: "image/png",
purpose: "any", purpose: "any",
}, },
{ {
src: getAssetUrl("/favicon-512x512.png"), src: "/favicon-512x512.png",
sizes: "512x512", sizes: "512x512",
type: "image/png", type: "image/png",
purpose: "any", purpose: "any",

View File

@@ -9,7 +9,6 @@ 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
@@ -80,7 +79,7 @@ export default function ExamplePanel({
setInput("Replicate this flowchart.") setInput("Replicate this flowchart.")
try { try {
const response = await fetch(getAssetUrl("/example.png")) const response = await fetch("/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])
@@ -93,7 +92,7 @@ export default function ExamplePanel({
setInput("Replicate this in aws style") setInput("Replicate this in aws style")
try { try {
const response = await fetch(getAssetUrl("/architecture.png")) const response = await fetch("/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",
@@ -108,7 +107,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(getAssetUrl("/chain-of-thought.txt")) const response = await fetch("/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",

View File

@@ -27,7 +27,6 @@ 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,
@@ -292,7 +291,7 @@ export function ChatMessageDisplay({
setFeedback((prev) => ({ ...prev, [messageId]: value })) setFeedback((prev) => ({ ...prev, [messageId]: value }))
try { try {
await fetch(getApiEndpoint("/api/log-feedback"), { await fetch("/api/log-feedback", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({

View File

@@ -21,21 +21,16 @@ import { ChatInput } from "@/components/chat-input"
import { ModelConfigDialog } from "@/components/model-config-dialog" import { ModelConfigDialog } from "@/components/model-config-dialog"
import { ResetWarningModal } from "@/components/reset-warning-modal" import { ResetWarningModal } from "@/components/reset-warning-modal"
import { SettingsDialog } from "@/components/settings-dialog" import { SettingsDialog } from "@/components/settings-dialog"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
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"
import { useQuotaManager } from "@/lib/use-quota-manager" import { useQuotaManager } from "@/lib/use-quota-manager"
import { formatXML, isMxCellXmlComplete, wrapWithMxFile } from "@/lib/utils" import { formatXML, isMxCellXmlComplete, wrapWithMxFile } from "@/lib/utils"
import { ChatMessageDisplay } from "./chat-message-display" import { ChatMessageDisplay } from "./chat-message-display"
import LanguageToggle from "./language-toggle"
// localStorage keys for persistence // localStorage keys for persistence
const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages" const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages"
@@ -173,7 +168,7 @@ export default function ChatPanel({
// Check config on mount // Check config on mount
useEffect(() => { useEffect(() => {
fetch(getApiEndpoint("/api/config")) fetch("/api/config")
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
setDailyRequestLimit(data.dailyRequestLimit || 0) setDailyRequestLimit(data.dailyRequestLimit || 0)
@@ -244,7 +239,7 @@ export default function ChatPanel({
setMessages, setMessages,
} = useChat({ } = useChat({
transport: new DefaultChatTransport({ transport: new DefaultChatTransport({
api: getApiEndpoint("/api/chat"), api: "/api/chat",
}), }),
async onToolCall({ toolCall }) { async onToolCall({ toolCall }) {
if (DEBUG) { if (DEBUG) {
@@ -1269,9 +1264,7 @@ Continue from EXACTLY where you stopped.`,
Next AI Drawio Next AI Drawio
</h1> </h1>
</div> </div>
{!isMobile && {!isMobile && (
process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE ===
"true" && (
<Link <Link
href="/about" href="/about"
target="_blank" target="_blank"
@@ -1281,9 +1274,7 @@ Continue from EXACTLY where you stopped.`,
About About
</Link> </Link>
)} )}
{!isMobile && {!isMobile && (
process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE ===
"true" && (
<Link <Link
href="/about" href="/about"
target="_blank" target="_blank"
@@ -1313,23 +1304,16 @@ Continue from EXACTLY where you stopped.`,
/> />
</ButtonWithTooltip> </ButtonWithTooltip>
<div className="w-px h-5 bg-border mx-1" /> <div className="w-px h-5 bg-border mx-1" />
<Tooltip>
<TooltipTrigger asChild>
<a <a
href="https://github.com/DayuanJiang/next-ai-draw-io" href="https://github.com/DayuanJiang/next-ai-draw-io"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center justify-center h-9 w-9 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors" className="p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
> >
<FaGithub <FaGithub
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`} className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
/> />
</a> </a>
</TooltipTrigger>
<TooltipContent>{dict.nav.github}</TooltipContent>
</Tooltip>
<ButtonWithTooltip <ButtonWithTooltip
tooltipContent={dict.nav.settings} tooltipContent={dict.nav.settings}
variant="ghost" variant="ghost"
@@ -1342,6 +1326,7 @@ Continue from EXACTLY where you stopped.`,
/> />
</ButtonWithTooltip> </ButtonWithTooltip>
<div className="hidden sm:flex items-center gap-2"> <div className="hidden sm:flex items-center gap-2">
<LanguageToggle />
{!isMobile && ( {!isMobile && (
<ButtonWithTooltip <ButtonWithTooltip
tooltipContent={dict.nav.hidePanel} tooltipContent={dict.nav.hidePanel}

View File

@@ -0,0 +1,108 @@
"use client"
import { Globe } from "lucide-react"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { Suspense, useEffect, useRef, useState } from "react"
import { i18n, type Locale } from "@/lib/i18n/config"
const LABELS: Record<string, string> = {
en: "EN",
zh: "中文",
ja: "日本語",
}
function LanguageToggleInner({ className = "" }: { className?: string }) {
const router = useRouter()
const pathname = usePathname() || "/"
const search = useSearchParams()
const [open, setOpen] = useState(false)
const [value, setValue] = useState<Locale>(i18n.defaultLocale)
const ref = useRef<HTMLDivElement | null>(null)
useEffect(() => {
const seg = pathname.split("/").filter(Boolean)
const first = seg[0]
if (first && i18n.locales.includes(first as Locale))
setValue(first as Locale)
else setValue(i18n.defaultLocale)
}, [pathname])
useEffect(() => {
function onDoc(e: MouseEvent) {
if (!ref.current) return
if (!ref.current.contains(e.target as Node)) setOpen(false)
}
if (open) document.addEventListener("mousedown", onDoc)
return () => document.removeEventListener("mousedown", onDoc)
}, [open])
const changeLocale = (lang: string) => {
const parts = pathname.split("/")
if (parts.length > 1 && i18n.locales.includes(parts[1] as Locale)) {
parts[1] = lang
} else {
parts.splice(1, 0, lang)
}
const newPath = parts.join("/") || "/"
const searchStr = search?.toString() ? `?${search.toString()}` : ""
setOpen(false)
router.push(newPath + searchStr)
}
return (
<div className={`relative inline-flex ${className}`} ref={ref}>
<button
aria-haspopup="menu"
aria-expanded={open}
onClick={() => setOpen((s) => !s)}
className="p-2 rounded-full hover:bg-accent/20 transition-colors text-muted-foreground"
aria-label="Change language"
>
<Globe className="w-5 h-5" />
</button>
{open && (
<div className="absolute right-0 top-full mt-2 w-40 bg-popover dark:bg-popover text-popover-foreground rounded-xl shadow-md border border-border/30 overflow-hidden z-50">
<div className="grid gap-0 divide-y divide-border/30">
{i18n.locales.map((loc) => (
<button
key={loc}
onClick={() => changeLocale(loc)}
className={`flex items-center gap-2 px-4 py-2 text-sm w-full text-left hover:bg-accent/10 transition-colors ${value === loc ? "bg-accent/10 font-semibold" : ""}`}
>
<span className="flex-1">
{LABELS[loc] ?? loc}
</span>
{value === loc && (
<span className="text-xs opacity-70">
</span>
)}
</button>
))}
</div>
</div>
)}
</div>
)
}
export default function LanguageToggle({
className = "",
}: {
className?: string
}) {
return (
<Suspense
fallback={
<button
className="p-2 rounded-full text-muted-foreground opacity-50"
disabled
>
<Globe className="w-5 h-5" />
</button>
}
>
<LanguageToggleInner className={className} />
</Suspense>
)
}

View File

@@ -1,8 +1,7 @@
"use client" "use client"
import { Moon, Sun } from "lucide-react" import { Moon, Sun } from "lucide-react"
import { usePathname, useRouter, useSearchParams } from "next/navigation" import { useEffect, useState } from "react"
import { Suspense, useEffect, useState } from "react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,
@@ -13,23 +12,8 @@ import {
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} 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"
const LANGUAGE_LABELS: Record<Locale, string> = {
en: "English",
zh: "中文",
ja: "日本語",
}
interface SettingsDialogProps { interface SettingsDialogProps {
open: boolean open: boolean
@@ -52,7 +36,7 @@ function getStoredAccessCodeRequired(): boolean | null {
return stored === "true" return stored === "true"
} }
function SettingsContent({ export function SettingsDialog({
open, open,
onOpenChange, onOpenChange,
onCloseProtectionChange, onCloseProtectionChange,
@@ -62,9 +46,6 @@ function SettingsContent({
onToggleDarkMode, onToggleDarkMode,
}: SettingsDialogProps) { }: SettingsDialogProps) {
const dict = useDictionary() const dict = useDictionary()
const router = useRouter()
const pathname = usePathname() || "/"
const search = useSearchParams()
const [accessCode, setAccessCode] = useState("") const [accessCode, setAccessCode] = useState("")
const [closeProtection, setCloseProtection] = useState(true) const [closeProtection, setCloseProtection] = useState(true)
const [isVerifying, setIsVerifying] = useState(false) const [isVerifying, setIsVerifying] = useState(false)
@@ -72,13 +53,12 @@ function SettingsContent({
const [accessCodeRequired, setAccessCodeRequired] = useState( const [accessCodeRequired, setAccessCodeRequired] = useState(
() => getStoredAccessCodeRequired() ?? false, () => getStoredAccessCodeRequired() ?? false,
) )
const [currentLang, setCurrentLang] = useState("en")
useEffect(() => { useEffect(() => {
// Only fetch if not cached in localStorage // Only fetch if not cached in localStorage
if (getStoredAccessCodeRequired() !== null) return if (getStoredAccessCodeRequired() !== null) return
fetch(getApiEndpoint("/api/config")) fetch("/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()
@@ -97,17 +77,6 @@ function SettingsContent({
}) })
}, []) }, [])
// Detect current language from pathname
useEffect(() => {
const seg = pathname.split("/").filter(Boolean)
const first = seg[0]
if (first && i18n.locales.includes(first as Locale)) {
setCurrentLang(first)
} else {
setCurrentLang(i18n.defaultLocale)
}
}, [pathname])
useEffect(() => { useEffect(() => {
if (open) { if (open) {
const storedCode = const storedCode =
@@ -124,18 +93,6 @@ function SettingsContent({
} }
}, [open]) }, [open])
const changeLanguage = (lang: string) => {
const parts = pathname.split("/")
if (parts.length > 1 && i18n.locales.includes(parts[1] as Locale)) {
parts[1] = lang
} else {
parts.splice(1, 0, lang)
}
const newPath = parts.join("/") || "/"
const searchStr = search?.toString() ? `?${search.toString()}` : ""
router.push(newPath + searchStr)
}
const handleSave = async () => { const handleSave = async () => {
if (!accessCodeRequired) return if (!accessCodeRequired) return
@@ -143,15 +100,12 @@ function SettingsContent({
setIsVerifying(true) setIsVerifying(true)
try { try {
const response = await fetch( const response = await fetch("/api/verify-access-code", {
getApiEndpoint("/api/verify-access-code"),
{
method: "POST", method: "POST",
headers: { headers: {
"x-access-code": accessCode.trim(), "x-access-code": accessCode.trim(),
}, },
}, })
)
const data = await response.json() const data = await response.json()
@@ -177,6 +131,7 @@ function SettingsContent({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>{dict.settings.title}</DialogTitle> <DialogTitle>{dict.settings.title}</DialogTitle>
@@ -195,7 +150,9 @@ function SettingsContent({
id="access-code" id="access-code"
type="password" type="password"
value={accessCode} value={accessCode}
onChange={(e) => setAccessCode(e.target.value)} onChange={(e) =>
setAccessCode(e.target.value)
}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder={ placeholder={
dict.settings.accessCodePlaceholder dict.settings.accessCodePlaceholder
@@ -219,30 +176,6 @@ function SettingsContent({
)} )}
</div> </div>
)} )}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="language-select">
{dict.settings.language}
</Label>
<p className="text-[0.8rem] text-muted-foreground">
{dict.settings.languageDescription}
</p>
</div>
<Select value={currentLang} onValueChange={changeLanguage}>
<SelectTrigger id="language-select" className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
{i18n.locales.map((locale) => (
<SelectItem key={locale} value={locale}>
{LANGUAGE_LABELS[locale]}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="space-y-0.5"> <div className="space-y-0.5">
<Label htmlFor="theme-toggle"> <Label htmlFor="theme-toggle">
@@ -320,23 +253,6 @@ function SettingsContent({
</p> </p>
</div> </div>
</DialogContent> </DialogContent>
)
}
export function SettingsDialog(props: SettingsDialogProps) {
return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<Suspense
fallback={
<DialogContent className="sm:max-w-md">
<div className="h-64 flex items-center justify-center">
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" />
</div>
</DialogContent>
}
>
<SettingsContent {...props} />
</Suspense>
</Dialog> </Dialog>
) )
} }

View File

@@ -5,7 +5,6 @@ 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 {
@@ -330,7 +329,7 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
sessionId?: string, sessionId?: string,
) => { ) => {
try { try {
await fetch(getApiEndpoint("/api/log-save"), { await fetch("/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 }),

View File

@@ -7,11 +7,6 @@ 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]

View File

@@ -68,10 +68,6 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
# SILICONFLOW_API_KEY=sk-... # SILICONFLOW_API_KEY=sk-...
# SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # Optional: switch to https://api.siliconflow.cn/v1 if needed # SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # Optional: switch to https://api.siliconflow.cn/v1 if needed
# SGLang Configuration (OpenAI-compatible)
# SGLANG_API_KEY=your-sglang-api-key
# SGLANG_BASE_URL=http://127.0.0.1:8000/v1 # Your SGLang endpoint
# Vercel AI Gateway Configuration # Vercel AI Gateway Configuration
# Get your API key from: https://vercel.com/ai-gateway # Get your API key from: https://vercel.com/ai-gateway
# Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5" # Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5"
@@ -97,12 +93,6 @@ 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.

View File

@@ -19,7 +19,6 @@ export type ProviderName =
| "openrouter" | "openrouter"
| "deepseek" | "deepseek"
| "siliconflow" | "siliconflow"
| "sglang"
| "gateway" | "gateway"
interface ModelConfig { interface ModelConfig {
@@ -51,7 +50,6 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
"openrouter", "openrouter",
"deepseek", "deepseek",
"siliconflow", "siliconflow",
"sglang",
"gateway", "gateway",
] ]
@@ -345,7 +343,6 @@ function buildProviderOptions(
case "deepseek": case "deepseek":
case "openrouter": case "openrouter":
case "siliconflow": case "siliconflow":
case "sglang":
case "gateway": { case "gateway": {
// These providers don't have reasoning configs in AI SDK yet // These providers don't have reasoning configs in AI SDK yet
// Gateway passes through to underlying providers which handle their own configs // Gateway passes through to underlying providers which handle their own configs
@@ -370,7 +367,6 @@ const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
openrouter: "OPENROUTER_API_KEY", openrouter: "OPENROUTER_API_KEY",
deepseek: "DEEPSEEK_API_KEY", deepseek: "DEEPSEEK_API_KEY",
siliconflow: "SILICONFLOW_API_KEY", siliconflow: "SILICONFLOW_API_KEY",
sglang: "SGLANG_API_KEY",
gateway: "AI_GATEWAY_API_KEY", gateway: "AI_GATEWAY_API_KEY",
} }
@@ -436,7 +432,7 @@ function validateProviderCredentials(provider: ProviderName): void {
* Get the AI model based on environment variables * Get the AI model based on environment variables
* *
* Environment variables: * Environment variables:
* - AI_PROVIDER: The provider to use (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway) * - AI_PROVIDER: The provider to use (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
* - AI_MODEL: The model ID/name for the selected provider * - AI_MODEL: The model ID/name for the selected provider
* *
* Provider-specific env vars: * Provider-specific env vars:
@@ -452,8 +448,6 @@ function validateProviderCredentials(provider: ProviderName): void {
* - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional) * - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional)
* - SILICONFLOW_API_KEY: SiliconFlow API key * - SILICONFLOW_API_KEY: SiliconFlow API key
* - SILICONFLOW_BASE_URL: SiliconFlow endpoint (optional, defaults to https://api.siliconflow.com/v1) * - SILICONFLOW_BASE_URL: SiliconFlow endpoint (optional, defaults to https://api.siliconflow.com/v1)
* - SGLANG_API_KEY: SGLang API key
* - SGLANG_BASE_URL: SGLang endpoint (optional)
*/ */
export function getAIModel(overrides?: ClientOverrides): ModelConfig { export function getAIModel(overrides?: ClientOverrides): ModelConfig {
// SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm) // SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm)
@@ -522,7 +516,6 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
`- OPENROUTER_API_KEY for OpenRouter\n` + `- OPENROUTER_API_KEY for OpenRouter\n` +
`- AZURE_API_KEY for Azure\n` + `- AZURE_API_KEY for Azure\n` +
`- SILICONFLOW_API_KEY for SiliconFlow\n` + `- SILICONFLOW_API_KEY for SiliconFlow\n` +
`- SGLANG_API_KEY for SGLang\n` +
`Or set AI_PROVIDER=ollama for local Ollama.`, `Or set AI_PROVIDER=ollama for local Ollama.`,
) )
} else { } else {
@@ -705,112 +698,6 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
break break
} }
case "sglang": {
const apiKey = overrides?.apiKey || process.env.SGLANG_API_KEY
const baseURL = overrides?.baseUrl || process.env.SGLANG_BASE_URL
const sglangProvider = createOpenAI({
apiKey,
baseURL,
// Add a custom fetch wrapper to intercept and fix the stream from sglang
fetch: async (url, options) => {
const response = await fetch(url, options)
if (!response.body) {
return response
}
// Create a transform stream to fix the non-compliant sglang stream
let buffer = ""
const decoder = new TextDecoder()
const transformStream = new TransformStream({
transform(chunk, controller) {
buffer += decoder.decode(chunk, { stream: true })
// Process all complete messages in the buffer
let messageEndPos
while (
(messageEndPos = buffer.indexOf("\n\n")) !== -1
) {
const message = buffer.substring(
0,
messageEndPos,
)
buffer = buffer.substring(messageEndPos + 2) // Move past the '\n\n'
if (message.startsWith("data: ")) {
const jsonStr = message.substring(6).trim()
if (jsonStr === "[DONE]") {
controller.enqueue(
new TextEncoder().encode(
message + "\n\n",
),
)
continue
}
try {
const data = JSON.parse(jsonStr)
const delta = data.choices?.[0]?.delta
if (delta) {
// Fix 1: remove invalid empty role
if (delta.role === "") {
delete delta.role
}
// Fix 2: remove non-standard reasoning_content field
if ("reasoning_content" in delta) {
delete delta.reasoning_content
}
}
// Re-serialize and forward the corrected data with the correct SSE format
controller.enqueue(
new TextEncoder().encode(
`data: ${JSON.stringify(data)}\n\n`,
),
)
} catch (e) {
// If parsing fails, forward the original message to avoid breaking the stream.
controller.enqueue(
new TextEncoder().encode(
message + "\n\n",
),
)
}
} else if (message.trim() !== "") {
// Pass through other message types (e.g., 'event: ...')
controller.enqueue(
new TextEncoder().encode(
message + "\n\n",
),
)
}
}
},
flush(controller) {
// If there's anything left in the buffer, forward it.
if (buffer.trim()) {
controller.enqueue(
new TextEncoder().encode(buffer),
)
}
},
})
const transformedBody =
response.body.pipeThrough(transformStream)
// Return a new response with the transformed body
return new Response(transformedBody, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
})
},
})
model = sglangProvider.chat(modelId)
break
}
case "gateway": { case "gateway": {
// Vercel AI Gateway - unified access to multiple AI providers // Vercel AI Gateway - unified access to multiple AI providers
// Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5" // Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5"
@@ -834,7 +721,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
default: default:
throw new Error( throw new Error(
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway`, `Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, gateway`,
) )
} }

View File

@@ -1,37 +0,0 @@
/**
* 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}`
}

View File

@@ -14,7 +14,6 @@
"about": "About", "about": "About",
"editor": "Editor", "editor": "Editor",
"newChat": "Start fresh chat", "newChat": "Start fresh chat",
"github": "GitHub",
"settings": "Settings", "settings": "Settings",
"hidePanel": "Hide chat panel (Ctrl+B)", "hidePanel": "Hide chat panel (Ctrl+B)",
"showPanel": "Show chat panel (Ctrl+B)", "showPanel": "Show chat panel (Ctrl+B)",
@@ -88,8 +87,6 @@
"overrides": "Overrides", "overrides": "Overrides",
"clearSettings": "Clear Settings", "clearSettings": "Clear Settings",
"useServerDefault": "Use Server Default", "useServerDefault": "Use Server Default",
"language": "Language",
"languageDescription": "Choose your interface language.",
"theme": "Theme", "theme": "Theme",
"themeDescription": "Dark/Light mode for interface and DrawIO canvas.", "themeDescription": "Dark/Light mode for interface and DrawIO canvas.",
"drawioStyle": "DrawIO Style", "drawioStyle": "DrawIO Style",
@@ -150,7 +147,6 @@
"tokenLimit": "Daily Token Limit Reached", "tokenLimit": "Daily Token Limit Reached",
"tpmLimit": "Rate Limit", "tpmLimit": "Rate Limit",
"tpmMessage": "Too many requests. Please wait a moment.", "tpmMessage": "Too many requests. Please wait a moment.",
"tpmMessageDetailed": "Rate limit reached ({limit} tokens/min). Please wait {seconds} seconds before sending another request.",
"messageApi": "Oops — you've reached the daily API limit for this demo! As an indie developer covering all the API costs myself, I have to set these limits to keep things sustainable.", "messageApi": "Oops — you've reached the daily API limit for this demo! As an indie developer covering all the API costs myself, I have to set these limits to keep things sustainable.",
"messageToken": "Oops — you've reached the daily token limit for this demo! As an indie developer covering all the API costs myself, I have to set these limits to keep things sustainable.", "messageToken": "Oops — you've reached the daily token limit for this demo! As an indie developer covering all the API costs myself, I have to set these limits to keep things sustainable.",
"tip": "<strong>Tip:</strong> You can use your own API key (click the Settings icon) or self-host the project to bypass these limits.", "tip": "<strong>Tip:</strong> You can use your own API key (click the Settings icon) or self-host the project to bypass these limits.",

View File

@@ -14,7 +14,6 @@
"about": "概要", "about": "概要",
"editor": "エディタ", "editor": "エディタ",
"newChat": "新しいチャットを開始", "newChat": "新しいチャットを開始",
"github": "GitHub",
"settings": "設定", "settings": "設定",
"hidePanel": "チャットパネルを非表示 (Ctrl+B)", "hidePanel": "チャットパネルを非表示 (Ctrl+B)",
"showPanel": "チャットパネルを表示 (Ctrl+B)", "showPanel": "チャットパネルを表示 (Ctrl+B)",
@@ -88,8 +87,6 @@
"overrides": "上書き", "overrides": "上書き",
"clearSettings": "設定をクリア", "clearSettings": "設定をクリア",
"useServerDefault": "サーバーデフォルトを使用", "useServerDefault": "サーバーデフォルトを使用",
"language": "言語",
"languageDescription": "インターフェース言語を選択します。",
"theme": "テーマ", "theme": "テーマ",
"themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。", "themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。",
"drawioStyle": "DrawIO スタイル", "drawioStyle": "DrawIO スタイル",
@@ -150,7 +147,6 @@
"tokenLimit": "1日のトークン制限に達しました", "tokenLimit": "1日のトークン制限に達しました",
"tpmLimit": "レート制限", "tpmLimit": "レート制限",
"tpmMessage": "リクエストが多すぎます。しばらくお待ちください。", "tpmMessage": "リクエストが多すぎます。しばらくお待ちください。",
"tpmMessageDetailed": "レート制限に達しました({limit}トークン/分)。{seconds}秒待ってからもう一度リクエストしてください。",
"messageApi": "おっと — このデモの1日の API 制限に達しました!個人開発者として API コストをすべて負担しているため、持続可能性を保つためにこれらの制限を設定する必要があります。", "messageApi": "おっと — このデモの1日の API 制限に達しました!個人開発者として API コストをすべて負担しているため、持続可能性を保つためにこれらの制限を設定する必要があります。",
"messageToken": "おっと — このデモの1日のトークン制限に達しました個人開発者として API コストをすべて負担しているため、持続可能性を保つためにこれらの制限を設定する必要があります。", "messageToken": "おっと — このデモの1日のトークン制限に達しました個人開発者として API コストをすべて負担しているため、持続可能性を保つためにこれらの制限を設定する必要があります。",
"tip": "<strong>ヒント:</strong>独自の API キーを使用する(設定アイコンをクリック)か、プロジェクトをセルフホストしてこれらの制限を回避できます。", "tip": "<strong>ヒント:</strong>独自の API キーを使用する(設定アイコンをクリック)か、プロジェクトをセルフホストしてこれらの制限を回避できます。",

View File

@@ -14,7 +14,6 @@
"about": "关于", "about": "关于",
"editor": "编辑器", "editor": "编辑器",
"newChat": "开始新对话", "newChat": "开始新对话",
"github": "GitHub",
"settings": "设置", "settings": "设置",
"hidePanel": "隐藏聊天面板 (Ctrl+B)", "hidePanel": "隐藏聊天面板 (Ctrl+B)",
"showPanel": "显示聊天面板 (Ctrl+B)", "showPanel": "显示聊天面板 (Ctrl+B)",
@@ -88,8 +87,6 @@
"overrides": "覆盖", "overrides": "覆盖",
"clearSettings": "清除设置", "clearSettings": "清除设置",
"useServerDefault": "使用服务器默认值", "useServerDefault": "使用服务器默认值",
"language": "语言",
"languageDescription": "选择界面语言。",
"theme": "主题", "theme": "主题",
"themeDescription": "界面和 DrawIO 画布的深色/浅色模式。", "themeDescription": "界面和 DrawIO 画布的深色/浅色模式。",
"drawioStyle": "DrawIO 样式", "drawioStyle": "DrawIO 样式",
@@ -150,7 +147,6 @@
"tokenLimit": "已达每日令牌限制", "tokenLimit": "已达每日令牌限制",
"tpmLimit": "速率限制", "tpmLimit": "速率限制",
"tpmMessage": "请求过多。请稍等片刻。", "tpmMessage": "请求过多。请稍等片刻。",
"tpmMessageDetailed": "达到速率限制({limit} 令牌/分钟)。请等待 {seconds} 秒后再发送请求。",
"messageApi": "糟糕 — 您已达到此演示的每日 API 限制!作为一名独立开发者,我自己承担所有 API 费用,因此必须设置这些限制以保持可持续性。", "messageApi": "糟糕 — 您已达到此演示的每日 API 限制!作为一名独立开发者,我自己承担所有 API 费用,因此必须设置这些限制以保持可持续性。",
"messageToken": "糟糕 — 您已达到此演示的每日令牌限制!作为一名独立开发者,我自己承担所有 API 费用,因此必须设置这些限制以保持可持续性。", "messageToken": "糟糕 — 您已达到此演示的每日令牌限制!作为一名独立开发者,我自己承担所有 API 费用,因此必须设置这些限制以保持可持续性。",
"tip": "<strong>提示:</strong>您可以使用自己的 API 密钥(点击设置图标)或自托管项目来绕过这些限制。", "tip": "<strong>提示:</strong>您可以使用自己的 API 密钥(点击设置图标)或自托管项目来绕过这些限制。",

View File

@@ -3,8 +3,6 @@
import { useCallback, useMemo } from "react" import { useCallback, useMemo } from "react"
import { toast } from "sonner" import { toast } from "sonner"
import { QuotaLimitToast } from "@/components/quota-limit-toast" import { QuotaLimitToast } from "@/components/quota-limit-toast"
import { useDictionary } from "@/hooks/use-dictionary"
import { formatMessage } from "@/lib/i18n/utils"
import { STORAGE_KEYS } from "@/lib/storage" import { STORAGE_KEYS } from "@/lib/storage"
export interface QuotaConfig { export interface QuotaConfig {
@@ -42,8 +40,6 @@ export function useQuotaManager(config: QuotaConfig): {
} { } {
const { dailyRequestLimit, dailyTokenLimit, tpmLimit } = config const { dailyRequestLimit, dailyTokenLimit, tpmLimit } = config
const dict = useDictionary()
// Check if user has their own API key configured (bypass limits) // Check if user has their own API key configured (bypass limits)
const hasOwnApiKey = useCallback((): boolean => { const hasOwnApiKey = useCallback((): boolean => {
const provider = localStorage.getItem(STORAGE_KEYS.aiProvider) const provider = localStorage.getItem(STORAGE_KEYS.aiProvider)
@@ -225,12 +221,11 @@ export function useQuotaManager(config: QuotaConfig): {
const showTPMLimitToast = useCallback(() => { const showTPMLimitToast = useCallback(() => {
const limitDisplay = const limitDisplay =
tpmLimit >= 1000 ? `${tpmLimit / 1000}k` : String(tpmLimit) tpmLimit >= 1000 ? `${tpmLimit / 1000}k` : String(tpmLimit)
const message = formatMessage(dict.quota.tpmMessageDetailed, { toast.error(
limit: limitDisplay, `Rate limit reached (${limitDisplay} tokens/min). Please wait 60 seconds before sending another request.`,
seconds: 60, { duration: 8000 },
}) )
toast.error(message, { duration: 8000 }) }, [tpmLimit])
}, [tpmLimit, dict])
return { return {
// Check functions // Check functions

View File

@@ -4,16 +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,
}, },
// Include instrumentation.ts in standalone build for Langfuse telemetry
outputFileTracingIncludes: {
"*": ["./instrumentation.ts"],
},
} }
export default nextConfig export default nextConfig

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "next-ai-draw-io", "name": "next-ai-draw-io",
"version": "0.4.6", "version": "0.4.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "next-ai-draw-io", "name": "next-ai-draw-io",
"version": "0.4.6", "version": "0.4.5",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@ai-sdk/amazon-bedrock": "^3.0.70", "@ai-sdk/amazon-bedrock": "^3.0.70",

View File

@@ -1,6 +1,6 @@
{ {
"name": "next-ai-draw-io", "name": "next-ai-draw-io",
"version": "0.4.6", "version": "0.4.5",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"main": "dist-electron/main/index.js", "main": "dist-electron/main/index.js",