mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-09 01:32:29 +08:00
Compare commits
2 Commits
6d84dade56
...
fix/move-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9d9dd257c | ||
|
|
9ac99a4690 |
@@ -242,6 +242,11 @@ Or you can deploy by this button.
|
|||||||
|
|
||||||
Be sure to **set the environment variables** in the Vercel dashboard as you did in your local `.env.local` file.
|
Be sure to **set the environment variables** in the Vercel dashboard as you did in your local `.env.local` file.
|
||||||
|
|
||||||
|
## Deploy on Cloudflare Workers
|
||||||
|
|
||||||
|
[Go to Cloudflare Deploy Guide](./docs/Cloudflare_Deploy.md)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Multi-Provider Support
|
## Multi-Provider Support
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import {
|
import { Image as ImageIcon, Loader2, Send, Trash2 } from "lucide-react"
|
||||||
Download,
|
|
||||||
History,
|
|
||||||
Image as ImageIcon,
|
|
||||||
Loader2,
|
|
||||||
Send,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react"
|
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
||||||
import { ErrorToast } from "@/components/error-toast"
|
import { ErrorToast } from "@/components/error-toast"
|
||||||
import { HistoryDialog } from "@/components/history-dialog"
|
|
||||||
import { ModelSelector } from "@/components/model-selector"
|
import { ModelSelector } from "@/components/model-selector"
|
||||||
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||||
import { SaveDialog } from "@/components/save-dialog"
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
@@ -25,7 +18,7 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip"
|
} from "@/components/ui/tooltip"
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import { formatMessage } from "@/lib/i18n/utils"
|
import { formatMessage } from "@/lib/i18n/utils"
|
||||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||||
@@ -152,8 +145,7 @@ interface ChatInputProps {
|
|||||||
File,
|
File,
|
||||||
{ text: string; charCount: number; isExtracting: boolean }
|
{ text: string; charCount: number; isExtracting: boolean }
|
||||||
>
|
>
|
||||||
showHistory?: boolean
|
|
||||||
onToggleHistory?: (show: boolean) => void
|
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
error?: Error | null
|
error?: Error | null
|
||||||
minimalStyle?: boolean
|
minimalStyle?: boolean
|
||||||
@@ -175,9 +167,7 @@ export function ChatInput({
|
|||||||
files = [],
|
files = [],
|
||||||
onFileChange = () => {},
|
onFileChange = () => {},
|
||||||
pdfData = new Map(),
|
pdfData = new Map(),
|
||||||
showHistory = false,
|
|
||||||
onToggleHistory = () => {},
|
|
||||||
sessionId,
|
|
||||||
error = null,
|
error = null,
|
||||||
minimalStyle = false,
|
minimalStyle = false,
|
||||||
onMinimalStyleChange = () => {},
|
onMinimalStyleChange = () => {},
|
||||||
@@ -188,12 +178,7 @@ export function ChatInput({
|
|||||||
onConfigureModels = () => {},
|
onConfigureModels = () => {},
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const dict = useDictionary()
|
const dict = useDictionary()
|
||||||
const {
|
|
||||||
diagramHistory,
|
|
||||||
saveDiagramToFile,
|
|
||||||
showSaveDialog,
|
|
||||||
setShowSaveDialog,
|
|
||||||
} = useDiagram()
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
@@ -386,11 +371,6 @@ export function ChatInput({
|
|||||||
onClear={handleClear}
|
onClear={handleClear}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HistoryDialog
|
|
||||||
showHistory={showHistory}
|
|
||||||
onToggleHistory={onToggleHistory}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
@@ -421,63 +401,29 @@ export function ChatInput({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1 overflow-hidden justify-end">
|
<div className="flex items-center gap-1 overflow-hidden justify-end">
|
||||||
<ButtonWithTooltip
|
<div className="flex items-center gap-1 overflow-x-hidden">
|
||||||
type="button"
|
<ButtonWithTooltip
|
||||||
variant="ghost"
|
type="button"
|
||||||
size="sm"
|
variant="ghost"
|
||||||
onClick={() => onToggleHistory(true)}
|
size="sm"
|
||||||
disabled={isDisabled || diagramHistory.length === 0}
|
onClick={triggerFileInput}
|
||||||
tooltipContent={dict.chat.diagramHistory}
|
disabled={isDisabled}
|
||||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
tooltipContent={dict.chat.uploadFile}
|
||||||
>
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||||
<History className="h-4 w-4" />
|
>
|
||||||
</ButtonWithTooltip>
|
<ImageIcon className="h-4 w-4" />
|
||||||
|
</ButtonWithTooltip>
|
||||||
<ButtonWithTooltip
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowSaveDialog(true)}
|
|
||||||
disabled={isDisabled}
|
|
||||||
tooltipContent={dict.chat.saveDiagram}
|
|
||||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
<Download className="h-4 w-4" />
|
|
||||||
</ButtonWithTooltip>
|
|
||||||
|
|
||||||
<SaveDialog
|
|
||||||
open={showSaveDialog}
|
|
||||||
onOpenChange={setShowSaveDialog}
|
|
||||||
onSave={(filename, format) =>
|
|
||||||
saveDiagramToFile(filename, format, sessionId)
|
|
||||||
}
|
|
||||||
defaultFilename={`diagram-${new Date()
|
|
||||||
.toISOString()
|
|
||||||
.slice(0, 10)}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ButtonWithTooltip
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={triggerFileInput}
|
|
||||||
disabled={isDisabled}
|
|
||||||
tooltipContent={dict.chat.uploadFile}
|
|
||||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
<ImageIcon className="h-4 w-4" />
|
|
||||||
</ButtonWithTooltip>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={fileInputRef}
|
|
||||||
className="hidden"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
accept="image/*,.pdf,application/pdf,text/*,.md,.markdown,.json,.csv,.xml,.yaml,.yml,.toml"
|
|
||||||
multiple
|
|
||||||
disabled={isDisabled}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept="image/*,.pdf,application/pdf,text/*,.md,.markdown,.json,.csv,.xml,.yaml,.yml,.toml"
|
||||||
|
multiple
|
||||||
|
disabled={isDisabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<ModelSelector
|
<ModelSelector
|
||||||
models={models}
|
models={models}
|
||||||
selectedModelId={selectedModelId}
|
selectedModelId={selectedModelId}
|
||||||
@@ -486,9 +432,7 @@ export function ChatInput({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
showUnvalidatedModels={showUnvalidatedModels}
|
showUnvalidatedModels={showUnvalidatedModels}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="w-px h-5 bg-border mx-1" />
|
<div className="w-px h-5 bg-border mx-1" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isDisabled || !input.trim()}
|
disabled={isDisabled || !input.trim()}
|
||||||
|
|||||||
@@ -154,7 +154,6 @@ export default function ChatPanel({
|
|||||||
// File processing using extracted hook
|
// File processing using extracted hook
|
||||||
const { files, pdfData, handleFileChange, setFiles } = useFileProcessor()
|
const { files, pdfData, handleFileChange, setFiles } = useFileProcessor()
|
||||||
|
|
||||||
const [showHistory, setShowHistory] = useState(false)
|
|
||||||
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
||||||
const [showModelConfigDialog, setShowModelConfigDialog] = useState(false)
|
const [showModelConfigDialog, setShowModelConfigDialog] = useState(false)
|
||||||
|
|
||||||
@@ -1066,8 +1065,6 @@ export default function ChatPanel({
|
|||||||
files={files}
|
files={files}
|
||||||
onFileChange={handleFileChange}
|
onFileChange={handleFileChange}
|
||||||
pdfData={pdfData}
|
pdfData={pdfData}
|
||||||
showHistory={showHistory}
|
|
||||||
onToggleHistory={setShowHistory}
|
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
error={error}
|
error={error}
|
||||||
minimalStyle={minimalStyle}
|
minimalStyle={minimalStyle}
|
||||||
@@ -1088,6 +1085,7 @@ export default function ChatPanel({
|
|||||||
onToggleDrawioUi={onToggleDrawioUi}
|
onToggleDrawioUi={onToggleDrawioUi}
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
onToggleDarkMode={onToggleDarkMode}
|
onToggleDarkMode={onToggleDarkMode}
|
||||||
|
sessionId={sessionId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ModelConfigDialog
|
<ModelConfigDialog
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Moon, Sun } from "lucide-react"
|
import { Download, History, Moon, Sun } from "lucide-react"
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||||
import { Suspense, useEffect, useState } from "react"
|
import { Suspense, useEffect, useState } from "react"
|
||||||
|
import { HistoryDialog } from "@/components/history-dialog"
|
||||||
|
import { type ExportFormat, SaveDialog } from "@/components/save-dialog"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -21,6 +23,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import { getApiEndpoint } from "@/lib/base-path"
|
import { getApiEndpoint } from "@/lib/base-path"
|
||||||
import { i18n, type Locale } from "@/lib/i18n/config"
|
import { i18n, type Locale } from "@/lib/i18n/config"
|
||||||
@@ -65,6 +68,7 @@ interface SettingsDialogProps {
|
|||||||
onToggleDrawioUi: () => void
|
onToggleDrawioUi: () => void
|
||||||
darkMode: boolean
|
darkMode: boolean
|
||||||
onToggleDarkMode: () => void
|
onToggleDarkMode: () => void
|
||||||
|
sessionId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
||||||
@@ -86,6 +90,7 @@ function SettingsContent({
|
|||||||
onToggleDrawioUi,
|
onToggleDrawioUi,
|
||||||
darkMode,
|
darkMode,
|
||||||
onToggleDarkMode,
|
onToggleDarkMode,
|
||||||
|
sessionId,
|
||||||
}: SettingsDialogProps) {
|
}: SettingsDialogProps) {
|
||||||
const dict = useDictionary()
|
const dict = useDictionary()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -95,11 +100,21 @@ function SettingsContent({
|
|||||||
const [closeProtection, setCloseProtection] = useState(true)
|
const [closeProtection, setCloseProtection] = useState(true)
|
||||||
const [isVerifying, setIsVerifying] = useState(false)
|
const [isVerifying, setIsVerifying] = useState(false)
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
|
const [showHistory, setShowHistory] = useState(false)
|
||||||
|
const [showSaveDialog, setShowSaveDialog] = useState(false)
|
||||||
const [accessCodeRequired, setAccessCodeRequired] = useState(
|
const [accessCodeRequired, setAccessCodeRequired] = useState(
|
||||||
() => getStoredAccessCodeRequired() ?? false,
|
() => getStoredAccessCodeRequired() ?? false,
|
||||||
)
|
)
|
||||||
const [currentLang, setCurrentLang] = useState("en")
|
const [currentLang, setCurrentLang] = useState("en")
|
||||||
|
|
||||||
|
// Get diagram context
|
||||||
|
const { diagramHistory, saveDiagramToFile } = useDiagram()
|
||||||
|
|
||||||
|
// Handler for saving diagram (RENAMED to avoid conflict)
|
||||||
|
const handleDiagramSave = (filename: string, format: ExportFormat) => {
|
||||||
|
saveDiagramToFile(filename, format, sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only fetch if not cached in localStorage
|
// Only fetch if not cached in localStorage
|
||||||
if (getStoredAccessCodeRequired() !== null) return
|
if (getStoredAccessCodeRequired() !== null) return
|
||||||
@@ -206,158 +221,211 @@ function SettingsContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent className="sm:max-w-lg p-0 gap-0">
|
<>
|
||||||
{/* Header */}
|
<DialogContent className="sm:max-w-lg p-0 gap-0">
|
||||||
<DialogHeader className="px-6 pt-6 pb-4">
|
{/* Header */}
|
||||||
<DialogTitle>{dict.settings.title}</DialogTitle>
|
<DialogHeader className="px-6 pt-6 pb-4">
|
||||||
<DialogDescription className="mt-1">
|
<DialogTitle>{dict.settings.title}</DialogTitle>
|
||||||
{dict.settings.description}
|
<DialogDescription className="mt-1">
|
||||||
</DialogDescription>
|
{dict.settings.description}
|
||||||
</DialogHeader>
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="px-6 pb-6">
|
<div className="px-6 pb-6">
|
||||||
<div className="divide-y divide-border-subtle">
|
<div className="divide-y divide-border-subtle">
|
||||||
{/* Access Code (conditional) */}
|
{/* Access Code (conditional) */}
|
||||||
{accessCodeRequired && (
|
{accessCodeRequired && (
|
||||||
<div className="py-4 first:pt-0 space-y-3">
|
<div className="py-4 first:pt-0 space-y-3">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label
|
<Label
|
||||||
htmlFor="access-code"
|
htmlFor="access-code"
|
||||||
className="text-sm font-medium"
|
className="text-sm font-medium"
|
||||||
>
|
>
|
||||||
{dict.settings.accessCode}
|
{dict.settings.accessCode}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{dict.settings.accessCodeDescription}
|
{dict.settings.accessCodeDescription}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="access-code"
|
||||||
|
type="password"
|
||||||
|
value={accessCode}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAccessCode(e.target.value)
|
||||||
|
}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={
|
||||||
|
dict.settings.accessCodePlaceholder
|
||||||
|
}
|
||||||
|
autoComplete="off"
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={
|
||||||
|
isVerifying || !accessCode.trim()
|
||||||
|
}
|
||||||
|
className="h-9 px-4 rounded-xl"
|
||||||
|
>
|
||||||
|
{isVerifying ? "..." : dict.common.save}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
)}
|
||||||
<Input
|
|
||||||
id="access-code"
|
{/* Language */}
|
||||||
type="password"
|
<SettingItem
|
||||||
value={accessCode}
|
label={dict.settings.language}
|
||||||
onChange={(e) =>
|
description={dict.settings.languageDescription}
|
||||||
setAccessCode(e.target.value)
|
>
|
||||||
}
|
<Select
|
||||||
onKeyDown={handleKeyDown}
|
value={currentLang}
|
||||||
placeholder={
|
onValueChange={changeLanguage}
|
||||||
dict.settings.accessCodePlaceholder
|
>
|
||||||
}
|
<SelectTrigger
|
||||||
autoComplete="off"
|
id="language-select"
|
||||||
className="h-9"
|
className="w-[120px] h-9 rounded-xl"
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={isVerifying || !accessCode.trim()}
|
|
||||||
className="h-9 px-4 rounded-xl"
|
|
||||||
>
|
>
|
||||||
{isVerifying ? "..." : dict.common.save}
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{i18n.locales.map((locale) => (
|
||||||
|
<SelectItem key={locale} value={locale}>
|
||||||
|
{LANGUAGE_LABELS[locale]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
{/* Theme */}
|
||||||
|
<SettingItem
|
||||||
|
label={dict.settings.theme}
|
||||||
|
description={dict.settings.themeDescription}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
id="theme-toggle"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={onToggleDarkMode}
|
||||||
|
className="h-9 w-9 rounded-xl border-border-subtle hover:bg-interactive-hover"
|
||||||
|
>
|
||||||
|
{darkMode ? (
|
||||||
|
<Sun className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Moon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
{/* Draw.io Style */}
|
||||||
|
<SettingItem
|
||||||
|
label={dict.settings.drawioStyle}
|
||||||
|
description={`${dict.settings.drawioStyleDescription} ${
|
||||||
|
drawioUi === "min"
|
||||||
|
? dict.settings.minimal
|
||||||
|
: dict.settings.sketch
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
id="drawio-ui"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onToggleDrawioUi}
|
||||||
|
className="h-9 w-[120px] rounded-xl border-border-subtle hover:bg-interactive-hover font-normal"
|
||||||
|
>
|
||||||
|
{dict.settings.switchTo}{" "}
|
||||||
|
{drawioUi === "min"
|
||||||
|
? dict.settings.sketch
|
||||||
|
: dict.settings.minimal}
|
||||||
|
</Button>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
{/* Close Protection */}
|
||||||
|
<SettingItem
|
||||||
|
label={dict.settings.closeProtection}
|
||||||
|
description={
|
||||||
|
dict.settings.closeProtectionDescription
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
id="close-protection"
|
||||||
|
checked={closeProtection}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setCloseProtection(checked)
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_CLOSE_PROTECTION_KEY,
|
||||||
|
checked.toString(),
|
||||||
|
)
|
||||||
|
onCloseProtectionChange?.(checked)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
{/* Diagram Actions */}
|
||||||
|
<SettingItem
|
||||||
|
label={dict.settings.diagramActions}
|
||||||
|
description={
|
||||||
|
dict.settings.diagramActionsDescription
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
id="history-button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowHistory(true)}
|
||||||
|
disabled={diagramHistory.length === 0}
|
||||||
|
className="h-9 rounded-xl border-border-subtle hover:bg-interactive-hover"
|
||||||
|
>
|
||||||
|
<History className="h-4 w-4 mr-1.5" />
|
||||||
|
{dict.settings.history}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
id="download-button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowSaveDialog(true)}
|
||||||
|
className="h-9 rounded-xl border-border-subtle hover:bg-interactive-hover"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 mr-1.5" />
|
||||||
|
{dict.settings.download}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
</SettingItem>
|
||||||
<p className="text-xs text-destructive">
|
</div>
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Language */}
|
|
||||||
<SettingItem
|
|
||||||
label={dict.settings.language}
|
|
||||||
description={dict.settings.languageDescription}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
value={currentLang}
|
|
||||||
onValueChange={changeLanguage}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id="language-select"
|
|
||||||
className="w-[120px] h-9 rounded-xl"
|
|
||||||
>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{i18n.locales.map((locale) => (
|
|
||||||
<SelectItem key={locale} value={locale}>
|
|
||||||
{LANGUAGE_LABELS[locale]}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</SettingItem>
|
|
||||||
|
|
||||||
{/* Theme */}
|
|
||||||
<SettingItem
|
|
||||||
label={dict.settings.theme}
|
|
||||||
description={dict.settings.themeDescription}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
id="theme-toggle"
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={onToggleDarkMode}
|
|
||||||
className="h-9 w-9 rounded-xl border-border-subtle hover:bg-interactive-hover"
|
|
||||||
>
|
|
||||||
{darkMode ? (
|
|
||||||
<Sun className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Moon className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</SettingItem>
|
|
||||||
|
|
||||||
{/* Draw.io Style */}
|
|
||||||
<SettingItem
|
|
||||||
label={dict.settings.drawioStyle}
|
|
||||||
description={`${dict.settings.drawioStyleDescription} ${
|
|
||||||
drawioUi === "min"
|
|
||||||
? dict.settings.minimal
|
|
||||||
: dict.settings.sketch
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
id="drawio-ui"
|
|
||||||
variant="outline"
|
|
||||||
onClick={onToggleDrawioUi}
|
|
||||||
className="h-9 w-[120px] rounded-xl border-border-subtle hover:bg-interactive-hover font-normal"
|
|
||||||
>
|
|
||||||
{dict.settings.switchTo}{" "}
|
|
||||||
{drawioUi === "min"
|
|
||||||
? dict.settings.sketch
|
|
||||||
: dict.settings.minimal}
|
|
||||||
</Button>
|
|
||||||
</SettingItem>
|
|
||||||
|
|
||||||
{/* Close Protection */}
|
|
||||||
<SettingItem
|
|
||||||
label={dict.settings.closeProtection}
|
|
||||||
description={dict.settings.closeProtectionDescription}
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
id="close-protection"
|
|
||||||
checked={closeProtection}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
setCloseProtection(checked)
|
|
||||||
localStorage.setItem(
|
|
||||||
STORAGE_CLOSE_PROTECTION_KEY,
|
|
||||||
checked.toString(),
|
|
||||||
)
|
|
||||||
onCloseProtectionChange?.(checked)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</SettingItem>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="px-6 py-4 border-t border-border-subtle bg-surface-1/50 rounded-b-2xl">
|
<div className="px-6 py-4 border-t border-border-subtle bg-surface-1/50 rounded-b-2xl">
|
||||||
<p className="text-xs text-muted-foreground text-center">
|
<p className="text-xs text-muted-foreground text-center">
|
||||||
Version {process.env.APP_VERSION}
|
Version {process.env.APP_VERSION}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
;(
|
||||||
|
<HistoryDialog
|
||||||
|
showHistory={showHistory}
|
||||||
|
onToggleHistory={setShowHistory}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
<SaveDialog
|
||||||
|
open={showSaveDialog}
|
||||||
|
onOpenChange={setShowSaveDialog}
|
||||||
|
onSave={handleDiagramSave}
|
||||||
|
defaultFilename={`diagram-${new Date()
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, 10)}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
267
docs/Cloudflare_Deploy.md
Normal file
267
docs/Cloudflare_Deploy.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# Deploy on Cloudflare Workers
|
||||||
|
|
||||||
|
This project can be deployed as a **Cloudflare Worker** using the **OpenNext adapter**, giving you:
|
||||||
|
|
||||||
|
- Global edge deployment
|
||||||
|
- Very low latency
|
||||||
|
- Free `workers.dev` hosting
|
||||||
|
- Full Next.js ISR support via R2 (optional)
|
||||||
|
|
||||||
|
> **Important Windows Note:** OpenNext and Wrangler are **not fully reliable on native Windows**. Recommended options:
|
||||||
|
>
|
||||||
|
> - Use **GitHub Codespaces** (works perfectly)
|
||||||
|
> - OR use **WSL (Linux)**
|
||||||
|
>
|
||||||
|
> Pure Windows builds may fail due to WASM file path issues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. A **Cloudflare account** (free tier works for basic deployment)
|
||||||
|
2. **Node.js 18+**
|
||||||
|
3. **Wrangler CLI** installed (dev dependency is fine):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -D wrangler
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Cloudflare login:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler login
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** A payment method is only required if you want to enable R2 for ISR caching. Basic Workers deployment is free.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1 — Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2 — Configure environment variables
|
||||||
|
|
||||||
|
Cloudflare uses a different file for local testing.
|
||||||
|
|
||||||
|
### 1) Create `.dev.vars` (for Cloudflare local + deploy)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .dev.vars
|
||||||
|
```
|
||||||
|
|
||||||
|
Fill in your API keys and configuration.
|
||||||
|
|
||||||
|
### 2) Make sure `.env.local` also exists (for regular Next.js dev)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Fill in the same values there.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 — Choose your deployment type
|
||||||
|
|
||||||
|
### Option A: Deploy WITHOUT R2 (Simple, Free)
|
||||||
|
|
||||||
|
If you don't need ISR caching, you can deploy without R2:
|
||||||
|
|
||||||
|
**1. Use simple `open-next.config.ts`:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
|
||||||
|
|
||||||
|
export default defineCloudflareConfig({})
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Use simple `wrangler.jsonc` (without r2_buckets):**
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"main": ".open-next/worker.js",
|
||||||
|
"name": "next-ai-draw-io-worker",
|
||||||
|
"compatibility_date": "2025-12-08",
|
||||||
|
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
||||||
|
"assets": {
|
||||||
|
"directory": ".open-next/assets",
|
||||||
|
"binding": "ASSETS"
|
||||||
|
},
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "next-ai-draw-io-worker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Skip to **Step 4**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option B: Deploy WITH R2 (Full ISR Support)
|
||||||
|
|
||||||
|
R2 enables **Incremental Static Regeneration (ISR)** caching. Requires a payment method on your Cloudflare account.
|
||||||
|
|
||||||
|
**1. Create an R2 bucket** in the Cloudflare Dashboard:
|
||||||
|
|
||||||
|
- Go to **Storage & Databases → R2**
|
||||||
|
- Click **Create bucket**
|
||||||
|
- Name it: `next-inc-cache`
|
||||||
|
|
||||||
|
**2. Configure `open-next.config.ts`:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
|
||||||
|
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"
|
||||||
|
|
||||||
|
export default defineCloudflareConfig({
|
||||||
|
incrementalCache: r2IncrementalCache,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Configure `wrangler.jsonc` (with R2):**
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"main": ".open-next/worker.js",
|
||||||
|
"name": "next-ai-draw-io-worker",
|
||||||
|
"compatibility_date": "2025-12-08",
|
||||||
|
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
||||||
|
"assets": {
|
||||||
|
"directory": ".open-next/assets",
|
||||||
|
"binding": "ASSETS"
|
||||||
|
},
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "next-inc-cache"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "next-ai-draw-io-worker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Important:** The `bucket_name` must exactly match the name you created in the Cloudflare dashboard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4 — Register a workers.dev subdomain (first-time only)
|
||||||
|
|
||||||
|
Before your first deployment, you need a workers.dev subdomain.
|
||||||
|
|
||||||
|
**Option 1: Via Cloudflare Dashboard (Recommended)**
|
||||||
|
|
||||||
|
Visit: https://dash.cloudflare.com → Workers & Pages → Overview → Set up a subdomain
|
||||||
|
|
||||||
|
**Option 2: During deploy**
|
||||||
|
|
||||||
|
When you run `npm run deploy`, Wrangler may prompt:
|
||||||
|
|
||||||
|
```
|
||||||
|
Would you like to register a workers.dev subdomain? (Y/n)
|
||||||
|
```
|
||||||
|
|
||||||
|
Type `Y` and choose a subdomain name.
|
||||||
|
|
||||||
|
> **Note:** In CI/CD or non-interactive environments, the prompt won't appear. Register via the dashboard first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5 — Deploy to Cloudflare
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
What the script does:
|
||||||
|
|
||||||
|
- Builds the Next.js app
|
||||||
|
- Converts it to a Cloudflare Worker via OpenNext
|
||||||
|
- Uploads static assets
|
||||||
|
- Publishes the Worker
|
||||||
|
|
||||||
|
Your app will be available at:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://<worker-name>.<your-subdomain>.workers.dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common issues & fixes
|
||||||
|
|
||||||
|
### `You need to register a workers.dev subdomain`
|
||||||
|
|
||||||
|
**Cause:** No workers.dev subdomain registered for your account.
|
||||||
|
|
||||||
|
**Fix:** Go to https://dash.cloudflare.com → Workers & Pages → Set up a subdomain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `Please enable R2 through the Cloudflare Dashboard`
|
||||||
|
|
||||||
|
**Cause:** R2 is configured in wrangler.jsonc but not enabled on your account.
|
||||||
|
|
||||||
|
**Fix:** Either enable R2 (requires payment method) or use Option A (deploy without R2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `No R2 binding "NEXT_INC_CACHE_R2_BUCKET" found`
|
||||||
|
|
||||||
|
**Cause:** `r2_buckets` is missing from `wrangler.jsonc`.
|
||||||
|
|
||||||
|
**Fix:** Add the `r2_buckets` section or switch to Option A (without R2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `Can't set compatibility date in the future`
|
||||||
|
|
||||||
|
**Cause:** `compatibility_date` in wrangler config is set to a future date.
|
||||||
|
|
||||||
|
**Fix:** Change `compatibility_date` to today or an earlier date.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Windows error: `resvg.wasm?module` (ENOENT)
|
||||||
|
|
||||||
|
**Cause:** Windows filenames cannot include `?`, but a wasm asset uses `?module` in its filename.
|
||||||
|
|
||||||
|
**Fix:** Build/deploy on Linux (WSL, Codespaces, or CI).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optional: Preview locally
|
||||||
|
|
||||||
|
Preview the Worker locally before deploying:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Feature | Without R2 | With R2 |
|
||||||
|
|---------|------------|---------|
|
||||||
|
| Cost | Free | Requires payment method |
|
||||||
|
| ISR Caching | No | Yes |
|
||||||
|
| Static Pages | Yes | Yes |
|
||||||
|
| API Routes | Yes | Yes |
|
||||||
|
| Setup Complexity | Simple | Moderate |
|
||||||
|
|
||||||
|
Choose **without R2** for testing or simple apps. Choose **with R2** for production apps that need ISR caching.
|
||||||
@@ -98,7 +98,11 @@
|
|||||||
"minimal": "Minimal",
|
"minimal": "Minimal",
|
||||||
"sketch": "Sketch",
|
"sketch": "Sketch",
|
||||||
"closeProtection": "Close Protection",
|
"closeProtection": "Close Protection",
|
||||||
"closeProtectionDescription": "Show confirmation when leaving the page."
|
"closeProtectionDescription": "Show confirmation when leaving the page.",
|
||||||
|
"diagramActions": "Diagram Actions",
|
||||||
|
"diagramActionsDescription": "Manage diagram history and exports",
|
||||||
|
"history": "History",
|
||||||
|
"download": "Download"
|
||||||
},
|
},
|
||||||
"save": {
|
"save": {
|
||||||
"title": "Save Diagram",
|
"title": "Save Diagram",
|
||||||
|
|||||||
@@ -98,7 +98,11 @@
|
|||||||
"minimal": "ミニマル",
|
"minimal": "ミニマル",
|
||||||
"sketch": "スケッチ",
|
"sketch": "スケッチ",
|
||||||
"closeProtection": "ページ離脱確認",
|
"closeProtection": "ページ離脱確認",
|
||||||
"closeProtectionDescription": "ページを離れる際に確認を表示します。"
|
"closeProtectionDescription": "ページを離れる際に確認を表示します。",
|
||||||
|
"diagramActions": "ダイアグラム操作",
|
||||||
|
"diagramActionsDescription": "ダイアグラムの履歴とエクスポートを管理",
|
||||||
|
"history": "履歴",
|
||||||
|
"download": "ダウンロード"
|
||||||
},
|
},
|
||||||
"save": {
|
"save": {
|
||||||
"title": "ダイアグラムを保存",
|
"title": "ダイアグラムを保存",
|
||||||
|
|||||||
@@ -98,7 +98,11 @@
|
|||||||
"minimal": "简约",
|
"minimal": "简约",
|
||||||
"sketch": "草图",
|
"sketch": "草图",
|
||||||
"closeProtection": "关闭确认",
|
"closeProtection": "关闭确认",
|
||||||
"closeProtectionDescription": "离开页面时显示确认。"
|
"closeProtectionDescription": "离开页面时显示确认。",
|
||||||
|
"diagramActions": "图表操作",
|
||||||
|
"diagramActionsDescription": "管理图表历史记录和导出",
|
||||||
|
"history": "历史记录",
|
||||||
|
"download": "下载"
|
||||||
},
|
},
|
||||||
"save": {
|
"save": {
|
||||||
"title": "保存图表",
|
"title": "保存图表",
|
||||||
|
|||||||
@@ -17,3 +17,13 @@ const nextConfig: NextConfig = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default nextConfig
|
export default nextConfig
|
||||||
|
|
||||||
|
// Initialize OpenNext Cloudflare for local development only
|
||||||
|
// This must be a dynamic import to avoid loading workerd binary during builds
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
import("@opennextjs/cloudflare").then(
|
||||||
|
({ initOpenNextCloudflareForDev }) => {
|
||||||
|
initOpenNextCloudflareForDev()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
7
open-next.config.ts
Normal file
7
open-next.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// default open-next.config.ts file created by @opennextjs/cloudflare
|
||||||
|
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
|
||||||
|
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"
|
||||||
|
|
||||||
|
export default defineCloudflareConfig({
|
||||||
|
incrementalCache: r2IncrementalCache,
|
||||||
|
})
|
||||||
6377
package-lock.json
generated
6377
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,10 @@
|
|||||||
"format": "biome check --write .",
|
"format": "biome check --write .",
|
||||||
"check": "biome ci",
|
"check": "biome ci",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
|
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||||
|
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||||
|
"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
||||||
|
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
|
||||||
"electron:dev": "node scripts/electron-dev.mjs",
|
"electron:dev": "node scripts/electron-dev.mjs",
|
||||||
"electron:build": "npm run build && npm run electron:compile",
|
"electron:build": "npm run build && npm run electron:compile",
|
||||||
"electron:compile": "npx esbuild electron/main/index.ts electron/preload/index.ts electron/preload/settings.ts --bundle --platform=node --outdir=dist-electron --external:electron --sourcemap --packages=external && npx shx cp -r electron/settings dist-electron/",
|
"electron:compile": "npx esbuild electron/main/index.ts electron/preload/index.ts electron/preload/settings.ts --bundle --platform=node --outdir=dist-electron --external:electron --sourcemap --packages=external && npx shx cp -r electron/settings dist-electron/",
|
||||||
@@ -39,6 +43,7 @@
|
|||||||
"@langfuse/otel": "^4.4.4",
|
"@langfuse/otel": "^4.4.4",
|
||||||
"@langfuse/tracing": "^4.4.9",
|
"@langfuse/tracing": "^4.4.9",
|
||||||
"@next/third-parties": "^16.0.6",
|
"@next/third-parties": "^16.0.6",
|
||||||
|
"@opennextjs/cloudflare": "1.14.7",
|
||||||
"@openrouter/ai-sdk-provider": "^1.5.4",
|
"@openrouter/ai-sdk-provider": "^1.5.4",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
|
||||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||||
@@ -111,7 +116,8 @@
|
|||||||
"shx": "^0.4.0",
|
"shx": "^0.4.0",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"wait-on": "^9.0.3"
|
"wait-on": "^9.0.3",
|
||||||
|
"wrangler": "4.54.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@openrouter/ai-sdk-provider": {
|
"@openrouter/ai-sdk-provider": {
|
||||||
|
|||||||
2
public/_headers
Normal file
2
public/_headers
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/_next/static/*
|
||||||
|
Cache-Control: public,max-age=31536000,immutable
|
||||||
23
wrangler.jsonc
Normal file
23
wrangler.jsonc
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"main": ".open-next/worker.js",
|
||||||
|
"name": "next-ai-draw-io-worker",
|
||||||
|
"compatibility_date": "2025-12-08", // must be a today or past compatibility_date
|
||||||
|
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
||||||
|
"assets": {
|
||||||
|
"directory": ".open-next/assets",
|
||||||
|
"binding": "ASSETS"
|
||||||
|
},
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "next-inc-cache"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "next-ai-draw-io-worker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user