mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-03 06:42: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:
@@ -4,9 +4,7 @@ import {
|
||||
Download,
|
||||
History,
|
||||
Image as ImageIcon,
|
||||
LayoutGrid,
|
||||
Loader2,
|
||||
PenTool,
|
||||
Send,
|
||||
Trash2,
|
||||
} from "lucide-react"
|
||||
@@ -19,14 +17,6 @@ import { HistoryDialog } from "@/components/history-dialog"
|
||||
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||
import { SaveDialog } from "@/components/save-dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { useDiagram } from "@/contexts/diagram-context"
|
||||
import { FilePreviewList } from "./file-preview-list"
|
||||
@@ -123,8 +113,6 @@ interface ChatInputProps {
|
||||
onToggleHistory?: (show: boolean) => void
|
||||
sessionId?: string
|
||||
error?: Error | null
|
||||
drawioUi?: "min" | "sketch"
|
||||
onToggleDrawioUi?: () => void
|
||||
}
|
||||
|
||||
export function ChatInput({
|
||||
@@ -139,8 +127,6 @@ export function ChatInput({
|
||||
onToggleHistory = () => {},
|
||||
sessionId,
|
||||
error = null,
|
||||
drawioUi = "min",
|
||||
onToggleDrawioUi = () => {},
|
||||
}: ChatInputProps) {
|
||||
const { diagramHistory, saveDiagramToFile } = useDiagram()
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
@@ -148,7 +134,6 @@ export function ChatInput({
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [showClearDialog, setShowClearDialog] = 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")
|
||||
const isDisabled =
|
||||
@@ -337,60 +322,6 @@ export function ChatInput({
|
||||
showHistory={showHistory}
|
||||
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>
|
||||
|
||||
{/* Right actions */}
|
||||
|
||||
@@ -135,6 +135,7 @@ export function ChatMessageDisplay({
|
||||
const copyMessageToClipboard = async (messageId: string, text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
|
||||
setCopiedMessageId(messageId)
|
||||
setTimeout(() => setCopiedMessageId(null), 2000)
|
||||
} catch (err) {
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages"
|
||||
const STORAGE_XML_SNAPSHOTS_KEY = "next-ai-draw-io-xml-snapshots"
|
||||
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_DATE_KEY = "next-ai-draw-io-request-date"
|
||||
const STORAGE_TOKEN_COUNT_KEY = "next-ai-draw-io-token-count"
|
||||
@@ -52,6 +52,8 @@ interface ChatPanelProps {
|
||||
onToggleVisibility: () => void
|
||||
drawioUi: "min" | "sketch"
|
||||
onToggleDrawioUi: () => void
|
||||
darkMode: boolean
|
||||
onToggleDarkMode: () => void
|
||||
isMobile?: boolean
|
||||
onCloseProtectionChange?: (enabled: boolean) => void
|
||||
}
|
||||
@@ -61,6 +63,8 @@ export default function ChatPanel({
|
||||
onToggleVisibility,
|
||||
drawioUi,
|
||||
onToggleDrawioUi,
|
||||
darkMode,
|
||||
onToggleDarkMode,
|
||||
isMobile = false,
|
||||
onCloseProtectionChange,
|
||||
}: ChatPanelProps) {
|
||||
@@ -582,13 +586,13 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
const hasDiagramRestoredRef = useRef(false)
|
||||
const [canSaveDiagram, setCanSaveDiagram] = useState(false)
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
"[ChatPanel] isDrawioReady:",
|
||||
isDrawioReady,
|
||||
"hasDiagramRestored:",
|
||||
hasDiagramRestoredRef.current,
|
||||
)
|
||||
if (!isDrawioReady || hasDiagramRestoredRef.current) return
|
||||
// Reset restore flag when DrawIO is not ready (e.g., theme/UI change remounts it)
|
||||
if (!isDrawioReady) {
|
||||
hasDiagramRestoredRef.current = false
|
||||
setCanSaveDiagram(false)
|
||||
return
|
||||
}
|
||||
if (hasDiagramRestoredRef.current) return
|
||||
hasDiagramRestoredRef.current = true
|
||||
|
||||
try {
|
||||
@@ -629,6 +633,14 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
}
|
||||
}, [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
|
||||
const saveXmlSnapshots = useCallback(() => {
|
||||
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)
|
||||
}, [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(() => {
|
||||
if (messagesEndRef.current) {
|
||||
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}
|
||||
sessionId={sessionId}
|
||||
error={error}
|
||||
drawioUi={drawioUi}
|
||||
onToggleDrawioUi={onToggleDrawioUi}
|
||||
/>
|
||||
</footer>
|
||||
|
||||
@@ -1213,6 +1209,10 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
open={showSettingsDialog}
|
||||
onOpenChange={setShowSettingsDialog}
|
||||
onCloseProtectionChange={onCloseProtectionChange}
|
||||
drawioUi={drawioUi}
|
||||
onToggleDrawioUi={onToggleDrawioUi}
|
||||
darkMode={darkMode}
|
||||
onToggleDarkMode={onToggleDarkMode}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
@@ -24,6 +25,10 @@ interface SettingsDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: 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"
|
||||
@@ -45,6 +50,10 @@ export function SettingsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onCloseProtectionChange,
|
||||
drawioUi,
|
||||
onToggleDrawioUi,
|
||||
darkMode,
|
||||
onToggleDarkMode,
|
||||
}: SettingsDialogProps) {
|
||||
const [accessCode, setAccessCode] = useState("")
|
||||
const [closeProtection, setCloseProtection] = useState(true)
|
||||
@@ -357,6 +366,47 @@ export function SettingsDialog({
|
||||
)}
|
||||
</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="space-y-0.5">
|
||||
<Label htmlFor="close-protection">
|
||||
|
||||
Reference in New Issue
Block a user