feat: make manual network map creation easier to discover

This commit is contained in:
chihlasm
2026-04-12 05:39:35 +00:00
parent 4527571d5f
commit 327a5c7c14

View File

@@ -1,5 +1,5 @@
import { useState, useCallback } from 'react' import { useState, useCallback, useEffect } from 'react'
import { Sparkles, ArrowRight, PencilRuler, Wand2 } from 'lucide-react' import { Sparkles, ArrowRight, PencilRuler, Wand2, X } from 'lucide-react'
import { networkDiagramsApi } from '@/api' import { networkDiagramsApi } from '@/api'
import type { AIGenerateResponse } from '@/types' import type { AIGenerateResponse } from '@/types'
@@ -21,6 +21,12 @@ export function CanvasEmptyPrompt({ onGenerate }: CanvasEmptyPromptProps) {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const switchToManual = useCallback(() => {
if (loading) return
setMode('manual')
setError(null)
}, [loading])
const handleGenerate = useCallback(async (text?: string) => { const handleGenerate = useCallback(async (text?: string) => {
const desc = (text ?? description).trim() const desc = (text ?? description).trim()
if (!desc) return if (!desc) return
@@ -40,16 +46,36 @@ export function CanvasEmptyPrompt({ onGenerate }: CanvasEmptyPromptProps) {
} }
}, [description, onGenerate]) }, [description, onGenerate])
useEffect(() => {
if (mode === 'manual') return
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
event.preventDefault()
switchToManual()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [mode, switchToManual])
if (mode === 'manual') { if (mode === 'manual') {
return ( return (
<div className="pointer-events-none absolute inset-x-0 bottom-6 z-10 flex justify-center px-6"> <div className="pointer-events-none absolute inset-x-0 bottom-6 z-10 flex justify-center px-6">
<div className="pointer-events-auto flex items-center gap-3 rounded-full border border-default bg-card/95 px-4 py-2 shadow-xl backdrop-blur"> <div className="pointer-events-auto flex max-w-xl items-center gap-3 rounded-2xl border border-default bg-card/95 px-4 py-3 shadow-xl backdrop-blur">
<span className="text-xs text-muted-foreground"> <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-accent/10 text-accent">
Manual mode enabled. Drag devices from the left panel to start building. <PencilRuler size={14} />
</span> </div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-heading">Manual mode is on</p>
<p className="text-xs text-muted-foreground">
Drag devices from the left panel onto the canvas, or reopen AI whenever you want.
</p>
</div>
<button <button
onClick={() => setMode('ai')} onClick={() => setMode('ai')}
className="inline-flex items-center gap-1 rounded-full border border-default px-3 py-1 text-xs font-medium text-primary hover:border-accent hover:text-accent" className="inline-flex shrink-0 items-center gap-1 rounded-full border border-default px-3 py-1 text-xs font-medium text-primary hover:border-accent hover:text-accent"
> >
<Sparkles size={12} /> <Sparkles size={12} />
Open AI Generator Open AI Generator
@@ -60,8 +86,23 @@ export function CanvasEmptyPrompt({ onGenerate }: CanvasEmptyPromptProps) {
} }
return ( return (
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center"> <div
<div className="pointer-events-auto w-full max-w-lg rounded-xl border border-default bg-card p-8 shadow-2xl"> className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center bg-[rgba(10,14,20,0.42)] px-6"
onClick={event => {
if (event.target === event.currentTarget) {
switchToManual()
}
}}
>
<div className="pointer-events-auto relative w-full max-w-lg rounded-xl border border-default bg-card p-8 shadow-2xl">
<button
onClick={switchToManual}
disabled={loading}
aria-label="Close AI prompt and build manually"
className="absolute right-4 top-4 inline-flex h-8 w-8 items-center justify-center rounded-full border border-default text-muted-foreground hover:border-hover hover:text-primary disabled:opacity-40"
>
<X size={14} />
</button>
{mode === 'choice' ? ( {mode === 'choice' ? (
<> <>
<div className="mb-6 text-center"> <div className="mb-6 text-center">
@@ -74,6 +115,9 @@ export function CanvasEmptyPrompt({ onGenerate }: CanvasEmptyPromptProps) {
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Generate a topology with AI or start with a blank canvas and build it manually. Generate a topology with AI or start with a blank canvas and build it manually.
</p> </p>
<p className="mt-2 text-[11px] text-muted-foreground/80">
Press <span className="font-medium text-primary">Esc</span> or click outside to skip AI and start dragging devices.
</p>
</div> </div>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
@@ -91,18 +135,26 @@ export function CanvasEmptyPrompt({ onGenerate }: CanvasEmptyPromptProps) {
</button> </button>
<button <button
onClick={() => setMode('manual')} onClick={switchToManual}
className="rounded-xl border border-default bg-elevated/30 p-4 text-left transition-colors hover:border-hover hover:bg-elevated/50" className="rounded-xl border-2 border-primary/20 bg-elevated/40 p-4 text-left transition-colors hover:border-accent hover:bg-elevated/60"
> >
<div className="mb-3 inline-flex rounded-lg bg-elevated p-2 text-primary"> <div className="mb-3 inline-flex rounded-lg bg-primary/10 p-2 text-primary">
<PencilRuler size={16} /> <PencilRuler size={16} />
</div> </div>
<div className="mb-1 text-sm font-semibold text-heading">Build manually</div> <div className="mb-1 text-sm font-semibold text-heading">Build manually</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Close the AI prompt and drag devices from the left panel onto the canvas. Close this prompt and use click-and-drag from the left toolbar to place devices on the canvas.
</p> </p>
</button> </button>
</div> </div>
<button
onClick={switchToManual}
className="mt-4 flex w-full items-center justify-center gap-2 rounded-lg border border-default px-4 py-3 text-sm font-medium text-primary hover:border-accent hover:text-accent"
>
<PencilRuler size={14} />
Skip AI and start dragging devices
</button>
</> </>
) : ( ) : (
<> <>
@@ -116,6 +168,9 @@ export function CanvasEmptyPrompt({ onGenerate }: CanvasEmptyPromptProps) {
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
AI will generate the topology in seconds, or you can go back and switch to manual creation. AI will generate the topology in seconds, or you can go back and switch to manual creation.
</p> </p>
<p className="mt-2 text-[11px] text-muted-foreground/80">
Press <span className="font-medium text-primary">Esc</span>, click outside, or use the close button to build manually instead.
</p>
</div> </div>
<div className="relative mb-3"> <div className="relative mb-3">
@@ -153,12 +208,9 @@ export function CanvasEmptyPrompt({ onGenerate }: CanvasEmptyPromptProps) {
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => { onClick={switchToManual}
setMode('manual')
setError(null)
}}
disabled={loading} disabled={loading}
className="flex-1 rounded-lg border border-default px-4 py-2.5 text-sm font-medium text-primary hover:border-hover disabled:opacity-40" className="flex-1 rounded-lg border border-default px-4 py-2.5 text-sm font-medium text-primary hover:border-accent hover:text-accent disabled:opacity-40"
> >
Build Manually Build Manually
</button> </button>