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 { Sparkles, ArrowRight, PencilRuler, Wand2 } from 'lucide-react'
import { useState, useCallback, useEffect } from 'react'
import { Sparkles, ArrowRight, PencilRuler, Wand2, X } from 'lucide-react'
import { networkDiagramsApi } from '@/api'
import type { AIGenerateResponse } from '@/types'
@@ -21,6 +21,12 @@ export function CanvasEmptyPrompt({ onGenerate }: CanvasEmptyPromptProps) {
const [loading, setLoading] = useState(false)
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 desc = (text ?? description).trim()
if (!desc) return
@@ -40,16 +46,36 @@ export function CanvasEmptyPrompt({ onGenerate }: CanvasEmptyPromptProps) {
}
}, [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') {
return (
<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">
<span className="text-xs text-muted-foreground">
Manual mode enabled. Drag devices from the left panel to start building.
</span>
<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">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-accent/10 text-accent">
<PencilRuler size={14} />
</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
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} />
Open AI Generator
@@ -60,8 +86,23 @@ export function CanvasEmptyPrompt({ onGenerate }: CanvasEmptyPromptProps) {
}
return (
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center">
<div className="pointer-events-auto w-full max-w-lg rounded-xl border border-default bg-card p-8 shadow-2xl">
<div
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' ? (
<>
<div className="mb-6 text-center">
@@ -74,6 +115,9 @@ export function CanvasEmptyPrompt({ onGenerate }: CanvasEmptyPromptProps) {
<p className="text-xs text-muted-foreground">
Generate a topology with AI or start with a blank canvas and build it manually.
</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 className="grid gap-3 sm:grid-cols-2">
@@ -91,18 +135,26 @@ export function CanvasEmptyPrompt({ onGenerate }: CanvasEmptyPromptProps) {
</button>
<button
onClick={() => setMode('manual')}
className="rounded-xl border border-default bg-elevated/30 p-4 text-left transition-colors hover:border-hover hover:bg-elevated/50"
onClick={switchToManual}
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} />
</div>
<div className="mb-1 text-sm font-semibold text-heading">Build manually</div>
<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>
</button>
</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">
AI will generate the topology in seconds, or you can go back and switch to manual creation.
</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 className="relative mb-3">
@@ -153,12 +208,9 @@ export function CanvasEmptyPrompt({ onGenerate }: CanvasEmptyPromptProps) {
<div className="flex gap-2">
<button
onClick={() => {
setMode('manual')
setError(null)
}}
onClick={switchToManual}
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
</button>