feat: make manual network map creation easier to discover
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user