- Colorize: semantic category colors for all device types (network=blue, security=orange, compute=emerald, endpoint=amber, storage=violet, cloud=cyan, infra=steel); better icons (Router, ShieldAlert, Boxes, Package, Gauge, PlugZap, Video, Radio); MiniMap uses category colors - Onboard: centered AI generate prompt on empty canvas with 5 MSP-specific example chips, ⌘↵ shortcut, spinner; AIAssistPanel only shown with nodes - Arrange: properties panel — status badge grid at top, fields grouped into Network (IP/Subnet/VLAN) and Hardware (Hostname/Vendor/Model/Role) sections - Delight: segmented topology color bar on listing cards; backend returns category_counts via single extra query on list endpoint - Harden: real PNG export via html-to-image + getNodesBounds/getViewportForBounds - Polish: ChevronDown replaces unicode ▾, click-outside for client filter, consistent spinner in empty prompt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
110 lines
4.2 KiB
TypeScript
110 lines
4.2 KiB
TypeScript
import { useState, useCallback } from 'react'
|
|
import { Sparkles, ArrowRight } from 'lucide-react'
|
|
import { networkDiagramsApi } from '@/api'
|
|
import type { AIGenerateResponse } from '@/types'
|
|
|
|
const EXAMPLE_PROMPTS = [
|
|
'Small office with firewall and core switch',
|
|
'Azure hybrid cloud with VPN gateway',
|
|
'Branch office connected to HQ via MPLS',
|
|
'Data center with redundant core switches',
|
|
'Remote workforce with Meraki and cloud apps',
|
|
]
|
|
|
|
interface CanvasEmptyPromptProps {
|
|
onGenerate: (result: AIGenerateResponse, mode: 'replace' | 'merge') => void
|
|
}
|
|
|
|
export function CanvasEmptyPrompt({ onGenerate }: CanvasEmptyPromptProps) {
|
|
const [description, setDescription] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const handleGenerate = useCallback(async (text?: string) => {
|
|
const desc = (text ?? description).trim()
|
|
if (!desc) return
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const result = await networkDiagramsApi.aiGenerate({
|
|
description: desc,
|
|
mode: 'replace',
|
|
existingBounds: null,
|
|
})
|
|
onGenerate(result, 'replace')
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : 'Generation failed. Please try again.')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [description, onGenerate])
|
|
|
|
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="mb-5 text-center">
|
|
<div className="mb-2 flex items-center justify-center gap-2">
|
|
<Sparkles size={16} className="text-accent" />
|
|
<h2 className="font-heading text-base font-semibold text-heading">
|
|
Describe your network
|
|
</h2>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
AI will generate the topology in seconds — or drag devices from the left panel to build manually.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="relative mb-3">
|
|
<textarea
|
|
value={description}
|
|
onChange={e => setDescription(e.target.value)}
|
|
onKeyDown={e => {
|
|
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) handleGenerate()
|
|
}}
|
|
placeholder="e.g. Small office with a firewall, core switch, 3 access points, a file server, and 20 workstations"
|
|
rows={3}
|
|
disabled={loading}
|
|
autoFocus
|
|
className="w-full resize-none rounded-lg border border-default bg-input px-4 py-3 pb-7 text-sm text-primary placeholder:text-muted-foreground focus:border-accent focus:outline-none disabled:opacity-50"
|
|
/>
|
|
<span className="pointer-events-none absolute bottom-2 right-3 text-[10px] text-muted-foreground/50">
|
|
⌘↵ to generate
|
|
</span>
|
|
</div>
|
|
|
|
<div className="mb-4 flex flex-wrap gap-1.5">
|
|
{EXAMPLE_PROMPTS.map(p => (
|
|
<button
|
|
key={p}
|
|
onClick={() => handleGenerate(p)}
|
|
disabled={loading}
|
|
className="rounded-full border border-default px-3 py-1 text-xs text-muted-foreground transition-colors hover:border-accent hover:text-accent disabled:opacity-40"
|
|
>
|
|
{p}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{error && <p className="mb-3 text-xs text-red-400">{error}</p>}
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center gap-2 py-2.5">
|
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-accent border-t-transparent" />
|
|
<span className="text-sm text-muted-foreground">Mapping your network…</span>
|
|
</div>
|
|
) : (
|
|
<button
|
|
onClick={() => handleGenerate()}
|
|
disabled={!description.trim()}
|
|
className="flex w-full items-center justify-center gap-2 rounded-lg bg-accent px-4 py-2.5 text-sm font-medium text-white transition-opacity hover:bg-accent/90 disabled:opacity-40"
|
|
>
|
|
<Sparkles size={14} />
|
|
Generate Diagram
|
|
<ArrowRight size={14} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|