mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
feat:light/dark mode switch (#138)
Summary - Adds browser theme detection on first visit using prefers-color-scheme media query - Renames localStorage key from dark-mode to next-ai-draw-io-dark-mode for consistency with other keys - Uses STORAGE_DIAGRAM_XML_KEY constant instead of hardcoded string in diagram-context.tsx Changes app/page.tsx: - On first visit (no saved preference), detect browser's color scheme preference - Update localStorage key to follow project naming convention (next-ai-draw-io-*) contexts/diagram-context.tsx: - Import STORAGE_DIAGRAM_XML_KEY from chat-panel.tsx - Replace hardcoded "next-ai-draw-io-diagram-xml" with the constant
This commit is contained in:
@@ -106,7 +106,7 @@ export default function RootLayout({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<script
|
<script
|
||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
|
|||||||
85
app/page.tsx
85
app/page.tsx
@@ -15,32 +15,59 @@ const drawioBaseUrl =
|
|||||||
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net"
|
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { drawioRef, handleDiagramExport, onDrawioLoad } = useDiagram()
|
const { drawioRef, handleDiagramExport, onDrawioLoad, resetDrawioReady } =
|
||||||
|
useDiagram()
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
const [isChatVisible, setIsChatVisible] = useState(true)
|
const [isChatVisible, setIsChatVisible] = useState(true)
|
||||||
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
||||||
const [isThemeLoaded, setIsThemeLoaded] = useState(false)
|
const [darkMode, setDarkMode] = useState(false)
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false)
|
||||||
// Load theme from localStorage after mount to avoid hydration mismatch
|
|
||||||
useEffect(() => {
|
|
||||||
const saved = localStorage.getItem("drawio-theme")
|
|
||||||
if (saved === "min" || saved === "sketch") {
|
|
||||||
setDrawioUi(saved)
|
|
||||||
}
|
|
||||||
setIsThemeLoaded(true)
|
|
||||||
}, [])
|
|
||||||
const [closeProtection, setCloseProtection] = useState(false)
|
const [closeProtection, setCloseProtection] = useState(false)
|
||||||
|
|
||||||
// Load close protection setting from localStorage after mount
|
|
||||||
useEffect(() => {
|
|
||||||
const saved = localStorage.getItem(STORAGE_CLOSE_PROTECTION_KEY)
|
|
||||||
// Default to false since auto-save handles persistence
|
|
||||||
if (saved === "true") {
|
|
||||||
setCloseProtection(true)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
const chatPanelRef = useRef<ImperativePanelHandle>(null)
|
const chatPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
|
|
||||||
|
// Load preferences from localStorage after mount
|
||||||
|
useEffect(() => {
|
||||||
|
const savedUi = localStorage.getItem("drawio-theme")
|
||||||
|
if (savedUi === "min" || savedUi === "sketch") {
|
||||||
|
setDrawioUi(savedUi)
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedDarkMode = localStorage.getItem("next-ai-draw-io-dark-mode")
|
||||||
|
if (savedDarkMode !== null) {
|
||||||
|
// Use saved preference
|
||||||
|
const isDark = savedDarkMode === "true"
|
||||||
|
setDarkMode(isDark)
|
||||||
|
document.documentElement.classList.toggle("dark", isDark)
|
||||||
|
} else {
|
||||||
|
// First visit: match browser preference
|
||||||
|
const prefersDark = window.matchMedia(
|
||||||
|
"(prefers-color-scheme: dark)",
|
||||||
|
).matches
|
||||||
|
setDarkMode(prefersDark)
|
||||||
|
document.documentElement.classList.toggle("dark", prefersDark)
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedCloseProtection = localStorage.getItem(
|
||||||
|
STORAGE_CLOSE_PROTECTION_KEY,
|
||||||
|
)
|
||||||
|
if (savedCloseProtection === "true") {
|
||||||
|
setCloseProtection(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoaded(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const toggleDarkMode = () => {
|
||||||
|
const newValue = !darkMode
|
||||||
|
setDarkMode(newValue)
|
||||||
|
localStorage.setItem("next-ai-draw-io-dark-mode", String(newValue))
|
||||||
|
document.documentElement.classList.toggle("dark", newValue)
|
||||||
|
// Reset so onDrawioLoad fires again after remount
|
||||||
|
resetDrawioReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check mobile
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkMobile = () => {
|
const checkMobile = () => {
|
||||||
setIsMobile(window.innerWidth < 768)
|
setIsMobile(window.innerWidth < 768)
|
||||||
@@ -64,6 +91,7 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keyboard shortcut for toggling chat panel
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if ((event.ctrlKey || event.metaKey) && event.key === "b") {
|
if ((event.ctrlKey || event.metaKey) && event.key === "b") {
|
||||||
@@ -77,7 +105,6 @@ export default function Home() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Show confirmation dialog when user tries to leave the page
|
// Show confirmation dialog when user tries to leave the page
|
||||||
// This helps prevent accidental navigation from browser back gestures
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!closeProtection) return
|
if (!closeProtection) return
|
||||||
|
|
||||||
@@ -105,10 +132,10 @@ export default function Home() {
|
|||||||
isMobile ? "p-1" : "p-2"
|
isMobile ? "p-1" : "p-2"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30 bg-white">
|
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30">
|
||||||
{isThemeLoaded ? (
|
{isLoaded ? (
|
||||||
<DrawIoEmbed
|
<DrawIoEmbed
|
||||||
key={drawioUi}
|
key={`${drawioUi}-${darkMode}`}
|
||||||
ref={drawioRef}
|
ref={drawioRef}
|
||||||
onExport={handleDiagramExport}
|
onExport={handleDiagramExport}
|
||||||
onLoad={onDrawioLoad}
|
onLoad={onDrawioLoad}
|
||||||
@@ -119,10 +146,11 @@ export default function Home() {
|
|||||||
libraries: false,
|
libraries: false,
|
||||||
saveAndExit: false,
|
saveAndExit: false,
|
||||||
noExitBtn: true,
|
noExitBtn: true,
|
||||||
|
dark: darkMode,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full flex items-center justify-center">
|
<div className="h-full w-full flex items-center justify-center bg-background">
|
||||||
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" />
|
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -149,11 +177,14 @@ export default function Home() {
|
|||||||
onToggleVisibility={toggleChatPanel}
|
onToggleVisibility={toggleChatPanel}
|
||||||
drawioUi={drawioUi}
|
drawioUi={drawioUi}
|
||||||
onToggleDrawioUi={() => {
|
onToggleDrawioUi={() => {
|
||||||
const newTheme =
|
const newUi =
|
||||||
drawioUi === "min" ? "sketch" : "min"
|
drawioUi === "min" ? "sketch" : "min"
|
||||||
localStorage.setItem("drawio-theme", newTheme)
|
localStorage.setItem("drawio-theme", newUi)
|
||||||
setDrawioUi(newTheme)
|
setDrawioUi(newUi)
|
||||||
|
resetDrawioReady()
|
||||||
}}
|
}}
|
||||||
|
darkMode={darkMode}
|
||||||
|
onToggleDarkMode={toggleDarkMode}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
onCloseProtectionChange={setCloseProtection}
|
onCloseProtectionChange={setCloseProtection}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
History,
|
History,
|
||||||
Image as ImageIcon,
|
Image as ImageIcon,
|
||||||
LayoutGrid,
|
|
||||||
Loader2,
|
Loader2,
|
||||||
PenTool,
|
|
||||||
Send,
|
Send,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
@@ -19,14 +17,6 @@ import { HistoryDialog } from "@/components/history-dialog"
|
|||||||
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||||
import { SaveDialog } from "@/components/save-dialog"
|
import { SaveDialog } from "@/components/save-dialog"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog"
|
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
import { FilePreviewList } from "./file-preview-list"
|
import { FilePreviewList } from "./file-preview-list"
|
||||||
@@ -123,8 +113,6 @@ interface ChatInputProps {
|
|||||||
onToggleHistory?: (show: boolean) => void
|
onToggleHistory?: (show: boolean) => void
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
error?: Error | null
|
error?: Error | null
|
||||||
drawioUi?: "min" | "sketch"
|
|
||||||
onToggleDrawioUi?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatInput({
|
export function ChatInput({
|
||||||
@@ -139,8 +127,6 @@ export function ChatInput({
|
|||||||
onToggleHistory = () => {},
|
onToggleHistory = () => {},
|
||||||
sessionId,
|
sessionId,
|
||||||
error = null,
|
error = null,
|
||||||
drawioUi = "min",
|
|
||||||
onToggleDrawioUi = () => {},
|
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const { diagramHistory, saveDiagramToFile } = useDiagram()
|
const { diagramHistory, saveDiagramToFile } = useDiagram()
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
@@ -148,7 +134,6 @@ export function ChatInput({
|
|||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [showClearDialog, setShowClearDialog] = useState(false)
|
const [showClearDialog, setShowClearDialog] = useState(false)
|
||||||
const [showSaveDialog, setShowSaveDialog] = useState(false)
|
const [showSaveDialog, setShowSaveDialog] = useState(false)
|
||||||
const [showThemeWarning, setShowThemeWarning] = useState(false)
|
|
||||||
|
|
||||||
// Allow retry when there's an error (even if status is still "streaming" or "submitted")
|
// Allow retry when there's an error (even if status is still "streaming" or "submitted")
|
||||||
const isDisabled =
|
const isDisabled =
|
||||||
@@ -337,60 +322,6 @@ export function ChatInput({
|
|||||||
showHistory={showHistory}
|
showHistory={showHistory}
|
||||||
onToggleHistory={onToggleHistory}
|
onToggleHistory={onToggleHistory}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ButtonWithTooltip
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowThemeWarning(true)}
|
|
||||||
tooltipContent={
|
|
||||||
drawioUi === "min"
|
|
||||||
? "Switch to Sketch theme"
|
|
||||||
: "Switch to Minimal theme"
|
|
||||||
}
|
|
||||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{drawioUi === "min" ? (
|
|
||||||
<PenTool className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<LayoutGrid className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</ButtonWithTooltip>
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
open={showThemeWarning}
|
|
||||||
onOpenChange={setShowThemeWarning}
|
|
||||||
>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Switch Theme?</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Switching themes will reload the diagram
|
|
||||||
editor and clear any unsaved changes.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() =>
|
|
||||||
setShowThemeWarning(false)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() => {
|
|
||||||
onClearChat()
|
|
||||||
onToggleDrawioUi()
|
|
||||||
setShowThemeWarning(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Switch Theme
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right actions */}
|
{/* Right actions */}
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ export function ChatMessageDisplay({
|
|||||||
const copyMessageToClipboard = async (messageId: string, text: string) => {
|
const copyMessageToClipboard = async (messageId: string, text: string) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
|
|
||||||
setCopiedMessageId(messageId)
|
setCopiedMessageId(messageId)
|
||||||
setTimeout(() => setCopiedMessageId(null), 2000)
|
setTimeout(() => setCopiedMessageId(null), 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages"
|
const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages"
|
||||||
const STORAGE_XML_SNAPSHOTS_KEY = "next-ai-draw-io-xml-snapshots"
|
const STORAGE_XML_SNAPSHOTS_KEY = "next-ai-draw-io-xml-snapshots"
|
||||||
const STORAGE_SESSION_ID_KEY = "next-ai-draw-io-session-id"
|
const STORAGE_SESSION_ID_KEY = "next-ai-draw-io-session-id"
|
||||||
const STORAGE_DIAGRAM_XML_KEY = "next-ai-draw-io-diagram-xml"
|
export const STORAGE_DIAGRAM_XML_KEY = "next-ai-draw-io-diagram-xml"
|
||||||
const STORAGE_REQUEST_COUNT_KEY = "next-ai-draw-io-request-count"
|
const STORAGE_REQUEST_COUNT_KEY = "next-ai-draw-io-request-count"
|
||||||
const STORAGE_REQUEST_DATE_KEY = "next-ai-draw-io-request-date"
|
const STORAGE_REQUEST_DATE_KEY = "next-ai-draw-io-request-date"
|
||||||
const STORAGE_TOKEN_COUNT_KEY = "next-ai-draw-io-token-count"
|
const STORAGE_TOKEN_COUNT_KEY = "next-ai-draw-io-token-count"
|
||||||
@@ -52,6 +52,8 @@ interface ChatPanelProps {
|
|||||||
onToggleVisibility: () => void
|
onToggleVisibility: () => void
|
||||||
drawioUi: "min" | "sketch"
|
drawioUi: "min" | "sketch"
|
||||||
onToggleDrawioUi: () => void
|
onToggleDrawioUi: () => void
|
||||||
|
darkMode: boolean
|
||||||
|
onToggleDarkMode: () => void
|
||||||
isMobile?: boolean
|
isMobile?: boolean
|
||||||
onCloseProtectionChange?: (enabled: boolean) => void
|
onCloseProtectionChange?: (enabled: boolean) => void
|
||||||
}
|
}
|
||||||
@@ -61,6 +63,8 @@ export default function ChatPanel({
|
|||||||
onToggleVisibility,
|
onToggleVisibility,
|
||||||
drawioUi,
|
drawioUi,
|
||||||
onToggleDrawioUi,
|
onToggleDrawioUi,
|
||||||
|
darkMode,
|
||||||
|
onToggleDarkMode,
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
onCloseProtectionChange,
|
onCloseProtectionChange,
|
||||||
}: ChatPanelProps) {
|
}: ChatPanelProps) {
|
||||||
@@ -582,13 +586,13 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
const hasDiagramRestoredRef = useRef(false)
|
const hasDiagramRestoredRef = useRef(false)
|
||||||
const [canSaveDiagram, setCanSaveDiagram] = useState(false)
|
const [canSaveDiagram, setCanSaveDiagram] = useState(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(
|
// Reset restore flag when DrawIO is not ready (e.g., theme/UI change remounts it)
|
||||||
"[ChatPanel] isDrawioReady:",
|
if (!isDrawioReady) {
|
||||||
isDrawioReady,
|
hasDiagramRestoredRef.current = false
|
||||||
"hasDiagramRestored:",
|
setCanSaveDiagram(false)
|
||||||
hasDiagramRestoredRef.current,
|
return
|
||||||
)
|
}
|
||||||
if (!isDrawioReady || hasDiagramRestoredRef.current) return
|
if (hasDiagramRestoredRef.current) return
|
||||||
hasDiagramRestoredRef.current = true
|
hasDiagramRestoredRef.current = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -629,6 +633,14 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
}
|
}
|
||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
|
// Save diagram XML to localStorage whenever it changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canSaveDiagram) return
|
||||||
|
if (chartXML && chartXML.length > 300) {
|
||||||
|
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML)
|
||||||
|
}
|
||||||
|
}, [chartXML, canSaveDiagram])
|
||||||
|
|
||||||
// Save XML snapshots to localStorage whenever they change
|
// Save XML snapshots to localStorage whenever they change
|
||||||
const saveXmlSnapshots = useCallback(() => {
|
const saveXmlSnapshots = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
@@ -650,20 +662,6 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
localStorage.setItem(STORAGE_SESSION_ID_KEY, sessionId)
|
localStorage.setItem(STORAGE_SESSION_ID_KEY, sessionId)
|
||||||
}, [sessionId])
|
}, [sessionId])
|
||||||
|
|
||||||
// Save current diagram XML to localStorage whenever it changes
|
|
||||||
// Only save after initial restore is complete and if it's not an empty diagram
|
|
||||||
useEffect(() => {
|
|
||||||
if (!canSaveDiagram) return
|
|
||||||
// Don't save empty diagrams (check for minimal content)
|
|
||||||
if (chartXML && chartXML.length > 300) {
|
|
||||||
console.log(
|
|
||||||
"[ChatPanel] Saving diagram to localStorage, length:",
|
|
||||||
chartXML.length,
|
|
||||||
)
|
|
||||||
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML)
|
|
||||||
}
|
|
||||||
}, [chartXML, canSaveDiagram])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messagesEndRef.current) {
|
if (messagesEndRef.current) {
|
||||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
||||||
@@ -1204,8 +1202,6 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
onToggleHistory={setShowHistory}
|
onToggleHistory={setShowHistory}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
error={error}
|
error={error}
|
||||||
drawioUi={drawioUi}
|
|
||||||
onToggleDrawioUi={onToggleDrawioUi}
|
|
||||||
/>
|
/>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
@@ -1213,6 +1209,10 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
open={showSettingsDialog}
|
open={showSettingsDialog}
|
||||||
onOpenChange={setShowSettingsDialog}
|
onOpenChange={setShowSettingsDialog}
|
||||||
onCloseProtectionChange={onCloseProtectionChange}
|
onCloseProtectionChange={onCloseProtectionChange}
|
||||||
|
drawioUi={drawioUi}
|
||||||
|
onToggleDrawioUi={onToggleDrawioUi}
|
||||||
|
darkMode={darkMode}
|
||||||
|
onToggleDarkMode={onToggleDarkMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { Moon, Sun } from "lucide-react"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
@@ -24,6 +25,10 @@ interface SettingsDialogProps {
|
|||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
onCloseProtectionChange?: (enabled: boolean) => void
|
onCloseProtectionChange?: (enabled: boolean) => void
|
||||||
|
drawioUi: "min" | "sketch"
|
||||||
|
onToggleDrawioUi: () => void
|
||||||
|
darkMode: boolean
|
||||||
|
onToggleDarkMode: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
||||||
@@ -45,6 +50,10 @@ export function SettingsDialog({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onCloseProtectionChange,
|
onCloseProtectionChange,
|
||||||
|
drawioUi,
|
||||||
|
onToggleDrawioUi,
|
||||||
|
darkMode,
|
||||||
|
onToggleDarkMode,
|
||||||
}: SettingsDialogProps) {
|
}: SettingsDialogProps) {
|
||||||
const [accessCode, setAccessCode] = useState("")
|
const [accessCode, setAccessCode] = useState("")
|
||||||
const [closeProtection, setCloseProtection] = useState(true)
|
const [closeProtection, setCloseProtection] = useState(true)
|
||||||
@@ -357,6 +366,47 @@ export function SettingsDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="theme-toggle">Theme</Label>
|
||||||
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
|
Dark/Light mode for interface and DrawIO canvas.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
id="theme-toggle"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={onToggleDarkMode}
|
||||||
|
>
|
||||||
|
{darkMode ? (
|
||||||
|
<Sun className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Moon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="drawio-ui">DrawIO Style</Label>
|
||||||
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
|
Canvas style:{" "}
|
||||||
|
{drawioUi === "min" ? "Minimal" : "Sketch"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
id="drawio-ui"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onToggleDrawioUi}
|
||||||
|
>
|
||||||
|
Switch to{" "}
|
||||||
|
{drawioUi === "min" ? "Sketch" : "Minimal"}
|
||||||
|
</Button>
|
||||||
|
</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="close-protection">
|
<Label htmlFor="close-protection">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { createContext, useContext, useRef, useState } from "react"
|
import { createContext, useContext, 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 type { ExportFormat } from "@/components/save-dialog"
|
import type { ExportFormat } from "@/components/save-dialog"
|
||||||
import { extractDiagramXML, validateMxCellStructure } from "../lib/utils"
|
import { extractDiagramXML, validateMxCellStructure } from "../lib/utils"
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ interface DiagramContextType {
|
|||||||
) => void
|
) => void
|
||||||
isDrawioReady: boolean
|
isDrawioReady: boolean
|
||||||
onDrawioLoad: () => void
|
onDrawioLoad: () => void
|
||||||
|
resetDrawioReady: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiagramContext = createContext<DiagramContextType | undefined>(undefined)
|
const DiagramContext = createContext<DiagramContextType | undefined>(undefined)
|
||||||
@@ -45,9 +47,16 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
// Only set ready state once to prevent infinite loops
|
// Only set ready state once to prevent infinite loops
|
||||||
if (hasCalledOnLoadRef.current) return
|
if (hasCalledOnLoadRef.current) return
|
||||||
hasCalledOnLoadRef.current = true
|
hasCalledOnLoadRef.current = true
|
||||||
console.log("[DiagramContext] DrawIO loaded, setting ready state")
|
// console.log("[DiagramContext] DrawIO loaded, setting ready state")
|
||||||
setIsDrawioReady(true)
|
setIsDrawioReady(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetDrawioReady = () => {
|
||||||
|
// console.log("[DiagramContext] Resetting DrawIO ready state")
|
||||||
|
hasCalledOnLoadRef.current = false
|
||||||
|
setIsDrawioReady(false)
|
||||||
|
}
|
||||||
|
|
||||||
// Track if we're expecting an export for file save (stores raw export data)
|
// Track if we're expecting an export for file save (stores raw export data)
|
||||||
const saveResolverRef = useRef<{
|
const saveResolverRef = useRef<{
|
||||||
resolver: ((data: string) => void) | null
|
resolver: ((data: string) => void) | null
|
||||||
@@ -171,6 +180,9 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
fileContent = xmlContent
|
fileContent = xmlContent
|
||||||
mimeType = "application/xml"
|
mimeType = "application/xml"
|
||||||
extension = ".drawio"
|
extension = ".drawio"
|
||||||
|
|
||||||
|
// Save to localStorage when user manually saves
|
||||||
|
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, xmlContent)
|
||||||
} else if (format === "png") {
|
} else if (format === "png") {
|
||||||
// PNG data comes as base64 data URL
|
// PNG data comes as base64 data URL
|
||||||
fileContent = exportData
|
fileContent = exportData
|
||||||
@@ -251,6 +263,7 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
saveDiagramToFile,
|
saveDiagramToFile,
|
||||||
isDrawioReady,
|
isDrawioReady,
|
||||||
onDrawioLoad,
|
onDrawioLoad,
|
||||||
|
resetDrawioReady,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user