mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Refactor feature idea cloud components
This commit is contained in:
File diff suppressed because it is too large
Load Diff
98
src/components/FeatureIdeaCloud/FeatureIdeaCanvas.tsx
Normal file
98
src/components/FeatureIdeaCloud/FeatureIdeaCanvas.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { MouseEvent as ReactMouseEvent } from 'react'
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
ConnectionMode,
|
||||
Controls,
|
||||
Connection,
|
||||
Edge,
|
||||
Node,
|
||||
Panel,
|
||||
} from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import { FeatureIdea, IdeaEdgeData } from './types'
|
||||
import { nodeTypes } from './nodes'
|
||||
import { FeatureIdeaDebugPanel } from './FeatureIdeaDebugPanel'
|
||||
import { FeatureIdeaTipsPanel } from './FeatureIdeaTipsPanel'
|
||||
import { FeatureIdeaToolbar } from './FeatureIdeaToolbar'
|
||||
|
||||
type FeatureIdeaCanvasProps = {
|
||||
nodes: Node[]
|
||||
edges: Edge<IdeaEdgeData>[]
|
||||
onNodesChangeWrapper: (changes: any) => void
|
||||
onEdgesChangeWrapper: (changes: any) => void
|
||||
onConnect: (connection: Connection) => void
|
||||
onReconnect: (oldEdge: Edge, newConnection: Connection) => void
|
||||
onReconnectStart: () => void
|
||||
onReconnectEnd: (_: MouseEvent | TouchEvent, edge: Edge) => void
|
||||
onEdgeClick: (event: ReactMouseEvent, edge: Edge<IdeaEdgeData>) => void
|
||||
onNodeDoubleClick: (event: ReactMouseEvent, node: Node<FeatureIdea>) => void
|
||||
debugPanelOpen: boolean
|
||||
setDebugPanelOpen: (open: boolean) => void
|
||||
handleAddIdea: () => void
|
||||
handleAddGroup: () => void
|
||||
handleGenerateIdeas: () => void
|
||||
safeIdeas: FeatureIdea[]
|
||||
}
|
||||
|
||||
export const FeatureIdeaCanvas = ({
|
||||
nodes,
|
||||
edges,
|
||||
onNodesChangeWrapper,
|
||||
onEdgesChangeWrapper,
|
||||
onConnect,
|
||||
onReconnect,
|
||||
onReconnectStart,
|
||||
onReconnectEnd,
|
||||
onEdgeClick,
|
||||
onNodeDoubleClick,
|
||||
debugPanelOpen,
|
||||
setDebugPanelOpen,
|
||||
handleAddIdea,
|
||||
handleAddGroup,
|
||||
handleGenerateIdeas,
|
||||
safeIdeas,
|
||||
}: FeatureIdeaCanvasProps) => (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChangeWrapper}
|
||||
onEdgesChange={onEdgesChangeWrapper}
|
||||
onConnect={onConnect}
|
||||
onReconnect={onReconnect}
|
||||
onReconnectStart={onReconnectStart}
|
||||
onReconnectEnd={onReconnectEnd}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
nodeTypes={nodeTypes}
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
reconnectRadius={20}
|
||||
fitView
|
||||
minZoom={0.2}
|
||||
maxZoom={2}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
defaultEdgeOptions={{
|
||||
type: 'default',
|
||||
animated: false,
|
||||
}}
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="hsl(var(--border))" />
|
||||
<Controls showInteractive={false} />
|
||||
|
||||
<FeatureIdeaToolbar
|
||||
debugPanelOpen={debugPanelOpen}
|
||||
setDebugPanelOpen={setDebugPanelOpen}
|
||||
handleGenerateIdeas={handleGenerateIdeas}
|
||||
handleAddGroup={handleAddGroup}
|
||||
handleAddIdea={handleAddIdea}
|
||||
/>
|
||||
|
||||
<FeatureIdeaTipsPanel />
|
||||
|
||||
{debugPanelOpen && (
|
||||
<Panel position="top-center" className="max-w-2xl">
|
||||
<FeatureIdeaDebugPanel nodes={nodes} edges={edges} safeIdeas={safeIdeas} onClose={() => setDebugPanelOpen(false)} />
|
||||
</Panel>
|
||||
)}
|
||||
</ReactFlow>
|
||||
)
|
||||
219
src/components/FeatureIdeaCloud/FeatureIdeaDebugPanel.tsx
Normal file
219
src/components/FeatureIdeaCloud/FeatureIdeaDebugPanel.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import { Edge, Node } from 'reactflow'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { FeatureIdea, IdeaEdgeData } from './types'
|
||||
|
||||
type FeatureIdeaDebugPanelProps = {
|
||||
nodes: Node[]
|
||||
edges: Edge<IdeaEdgeData>[]
|
||||
safeIdeas: FeatureIdea[]
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
type HandleStats = {
|
||||
leftHandles: Edge<IdeaEdgeData>[]
|
||||
rightHandles: Edge<IdeaEdgeData>[]
|
||||
topHandles: Edge<IdeaEdgeData>[]
|
||||
bottomHandles: Edge<IdeaEdgeData>[]
|
||||
leftUnique: number
|
||||
rightUnique: number
|
||||
topUnique: number
|
||||
bottomUnique: number
|
||||
nodeEdges: Edge<IdeaEdgeData>[]
|
||||
}
|
||||
|
||||
const getHandleStats = (ideaId: string, edges: Edge<IdeaEdgeData>[]): HandleStats => {
|
||||
const nodeEdges = edges.filter((edge) => edge.source === ideaId || edge.target === ideaId)
|
||||
const leftHandles = edges.filter((edge) => edge.target === ideaId && edge.targetHandle?.startsWith('left'))
|
||||
const rightHandles = edges.filter((edge) => edge.source === ideaId && edge.sourceHandle?.startsWith('right'))
|
||||
const topHandles = edges.filter((edge) => edge.target === ideaId && edge.targetHandle?.startsWith('top'))
|
||||
const bottomHandles = edges.filter((edge) => edge.source === ideaId && edge.sourceHandle?.startsWith('bottom'))
|
||||
|
||||
return {
|
||||
leftHandles,
|
||||
rightHandles,
|
||||
topHandles,
|
||||
bottomHandles,
|
||||
leftUnique: new Set(leftHandles.map((edge) => edge.targetHandle)).size,
|
||||
rightUnique: new Set(rightHandles.map((edge) => edge.sourceHandle)).size,
|
||||
topUnique: new Set(topHandles.map((edge) => edge.targetHandle)).size,
|
||||
bottomUnique: new Set(bottomHandles.map((edge) => edge.sourceHandle)).size,
|
||||
nodeEdges,
|
||||
}
|
||||
}
|
||||
|
||||
const DebugSummary = ({ nodes, edges, safeIdeas }: Omit<FeatureIdeaDebugPanelProps, 'onClose'>) => (
|
||||
<div className="grid grid-cols-3 gap-4 p-3 bg-muted/50 rounded-lg text-xs">
|
||||
<div>
|
||||
<div className="font-semibold text-foreground mb-1">Total Edges</div>
|
||||
<div className="text-2xl font-bold text-primary">{edges.length}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-foreground mb-1">Total Nodes</div>
|
||||
<div className="text-2xl font-bold text-accent">{nodes.length}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-foreground mb-1">Total Ideas</div>
|
||||
<div className="text-2xl font-bold text-secondary">{safeIdeas.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const ConnectionMatrix = ({ safeIdeas, edges }: Pick<FeatureIdeaDebugPanelProps, 'safeIdeas' | 'edges'>) => (
|
||||
<div className="space-y-2">
|
||||
<div className="font-semibold text-xs flex items-center justify-between">
|
||||
<span>Connection Matrix (Handle Occupancy)</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
1:1 Constraint Active
|
||||
</Badge>
|
||||
</div>
|
||||
<ScrollArea className="h-48 w-full rounded-md border">
|
||||
<div className="p-2 space-y-2 text-xs font-mono">
|
||||
{safeIdeas.slice(0, 10).map((idea) => {
|
||||
const { nodeEdges, leftHandles, rightHandles, topHandles, bottomHandles, leftUnique, rightUnique, topUnique, bottomUnique } =
|
||||
getHandleStats(idea.id, edges)
|
||||
|
||||
const hasViolation =
|
||||
leftHandles.length !== leftUnique ||
|
||||
rightHandles.length !== rightUnique ||
|
||||
topHandles.length !== topUnique ||
|
||||
bottomHandles.length !== bottomUnique
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idea.id}
|
||||
className={`p-2 rounded border ${hasViolation ? 'bg-red-500/20 border-red-500' : 'bg-muted/30'}`}
|
||||
>
|
||||
<div className="font-semibold truncate mb-1 flex items-center gap-2" title={idea.title}>
|
||||
{hasViolation && <span className="text-red-500">⚠️</span>}
|
||||
{idea.title}
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-1 text-[10px]">
|
||||
<div
|
||||
className={`p-1 rounded text-center ${
|
||||
leftHandles.length !== leftUnique
|
||||
? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold'
|
||||
: leftHandles.length >= 1
|
||||
? 'bg-green-500/20 text-green-700 dark:text-green-300'
|
||||
: 'bg-muted'
|
||||
}`}
|
||||
>
|
||||
← {leftHandles.length}/{leftUnique} {leftHandles.length !== leftUnique ? '⚠️' : leftHandles.length > 0 ? '✓' : '○'}
|
||||
</div>
|
||||
<div
|
||||
className={`p-1 rounded text-center ${
|
||||
rightHandles.length !== rightUnique
|
||||
? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold'
|
||||
: rightHandles.length >= 1
|
||||
? 'bg-green-500/20 text-green-700 dark:text-green-300'
|
||||
: 'bg-muted'
|
||||
}`}
|
||||
>
|
||||
→ {rightHandles.length}/{rightUnique} {rightHandles.length !== rightUnique ? '⚠️' : rightHandles.length > 0 ? '✓' : '○'}
|
||||
</div>
|
||||
<div
|
||||
className={`p-1 rounded text-center ${
|
||||
topHandles.length !== topUnique
|
||||
? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold'
|
||||
: topHandles.length >= 1
|
||||
? 'bg-green-500/20 text-green-700 dark:text-green-300'
|
||||
: 'bg-muted'
|
||||
}`}
|
||||
>
|
||||
↑ {topHandles.length}/{topUnique} {topHandles.length !== topUnique ? '⚠️' : topHandles.length > 0 ? '✓' : '○'}
|
||||
</div>
|
||||
<div
|
||||
className={`p-1 rounded text-center ${
|
||||
bottomHandles.length !== bottomUnique
|
||||
? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold'
|
||||
: bottomHandles.length >= 1
|
||||
? 'bg-green-500/20 text-green-700 dark:text-green-300'
|
||||
: 'bg-muted'
|
||||
}`}
|
||||
>
|
||||
↓ {bottomHandles.length}/{bottomUnique} {bottomHandles.length !== bottomUnique ? '⚠️' : bottomHandles.length > 0 ? '✓' : '○'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 text-[10px] text-muted-foreground">
|
||||
Total: {nodeEdges.length} connection{nodeEdges.length !== 1 ? 's' : ''}, Handles: L{leftUnique}|R{rightUnique}|T
|
||||
{topUnique}|B{bottomUnique}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{safeIdeas.length > 10 && (
|
||||
<div className="text-center text-muted-foreground py-2">... and {safeIdeas.length - 10} more ideas</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
|
||||
const ConstraintStatus = ({ safeIdeas, edges }: Pick<FeatureIdeaDebugPanelProps, 'safeIdeas' | 'edges'>) => {
|
||||
const violations: string[] = []
|
||||
|
||||
safeIdeas.forEach((idea) => {
|
||||
const { leftHandles, rightHandles, topHandles, bottomHandles, leftUnique, rightUnique, topUnique, bottomUnique } = getHandleStats(
|
||||
idea.id,
|
||||
edges
|
||||
)
|
||||
|
||||
if (leftHandles.length !== leftUnique) {
|
||||
violations.push(`${idea.title}: Left side has duplicate handle usage (${leftHandles.length} connections on ${leftUnique} handles)`)
|
||||
}
|
||||
if (rightHandles.length !== rightUnique) {
|
||||
violations.push(`${idea.title}: Right side has duplicate handle usage (${rightHandles.length} connections on ${rightUnique} handles)`)
|
||||
}
|
||||
if (topHandles.length !== topUnique) {
|
||||
violations.push(`${idea.title}: Top side has duplicate handle usage (${topHandles.length} connections on ${topUnique} handles)`)
|
||||
}
|
||||
if (bottomHandles.length !== bottomUnique) {
|
||||
violations.push(`${idea.title}: Bottom side has duplicate handle usage (${bottomHandles.length} connections on ${bottomUnique} handles)`)
|
||||
}
|
||||
})
|
||||
|
||||
if (violations.length > 0) {
|
||||
return (
|
||||
<div className="space-y-1 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<div className="font-semibold text-xs text-red-700 dark:text-red-300 flex items-center gap-1">❌ CONSTRAINT VIOLATIONS DETECTED</div>
|
||||
<div className="text-xs space-y-0.5 text-red-600 dark:text-red-400">
|
||||
{violations.map((violation, index) => (
|
||||
<div key={index}>• {violation}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<div className="font-semibold text-xs text-green-700 dark:text-green-300 flex items-center gap-1">✅ All Constraints Satisfied</div>
|
||||
<div className="text-xs space-y-0.5">
|
||||
<div>• Each handle has exactly 1 connection (1:1 mapping) ✓</div>
|
||||
<div>• New connections automatically remove conflicts ✓</div>
|
||||
<div>• Spare blank handles always available ✓</div>
|
||||
<div>• Remapping preserves 1:1 constraint ✓</div>
|
||||
<div>• Changes persist to database immediately ✓</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const FeatureIdeaDebugPanel = ({ nodes, edges, safeIdeas, onClose }: FeatureIdeaDebugPanelProps) => (
|
||||
<Card className="shadow-2xl border-2">
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-bold text-sm">🔍 Connection Debug Panel</h3>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={onClose}>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DebugSummary nodes={nodes} edges={edges} safeIdeas={safeIdeas} />
|
||||
<ConnectionMatrix safeIdeas={safeIdeas} edges={edges} />
|
||||
<ConstraintStatus safeIdeas={safeIdeas} edges={edges} />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
108
src/components/FeatureIdeaCloud/FeatureIdeaDialogs.tsx
Normal file
108
src/components/FeatureIdeaCloud/FeatureIdeaDialogs.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Edge } from 'reactflow'
|
||||
import { FeatureIdea, IdeaEdgeData, IdeaGroup } from './types'
|
||||
import { EdgeDialog } from './dialogs/EdgeDialog'
|
||||
import { GroupDialog } from './dialogs/GroupDialog'
|
||||
import { IdeaEditDialog } from './dialogs/IdeaEditDialog'
|
||||
import { IdeaViewDialog } from './dialogs/IdeaViewDialog'
|
||||
|
||||
type FeatureIdeaDialogsProps = {
|
||||
ideas: FeatureIdea[] | null
|
||||
groups: IdeaGroup[] | null
|
||||
safeIdeas: FeatureIdea[]
|
||||
safeGroups: IdeaGroup[]
|
||||
edges: Edge<IdeaEdgeData>[]
|
||||
selectedIdea: FeatureIdea | null
|
||||
selectedGroup: IdeaGroup | null
|
||||
selectedEdge: Edge<IdeaEdgeData> | null
|
||||
editDialogOpen: boolean
|
||||
groupDialogOpen: boolean
|
||||
viewDialogOpen: boolean
|
||||
edgeDialogOpen: boolean
|
||||
setSelectedIdea: (idea: FeatureIdea | null) => void
|
||||
setSelectedGroup: (group: IdeaGroup | null) => void
|
||||
setSelectedEdge: (edge: Edge<IdeaEdgeData> | null) => void
|
||||
setEditDialogOpen: (open: boolean) => void
|
||||
setGroupDialogOpen: (open: boolean) => void
|
||||
setViewDialogOpen: (open: boolean) => void
|
||||
setEdgeDialogOpen: (open: boolean) => void
|
||||
handleSaveIdea: () => void
|
||||
handleDeleteIdea: (id: string) => void
|
||||
handleSaveGroup: () => void
|
||||
handleDeleteGroup: (id: string) => void
|
||||
handleDeleteEdge: (edgeId: string) => void
|
||||
handleSaveEdge: () => void
|
||||
}
|
||||
|
||||
export const FeatureIdeaDialogs = ({
|
||||
ideas,
|
||||
groups,
|
||||
safeIdeas,
|
||||
safeGroups,
|
||||
edges,
|
||||
selectedIdea,
|
||||
selectedGroup,
|
||||
selectedEdge,
|
||||
editDialogOpen,
|
||||
groupDialogOpen,
|
||||
viewDialogOpen,
|
||||
edgeDialogOpen,
|
||||
setSelectedIdea,
|
||||
setSelectedGroup,
|
||||
setSelectedEdge,
|
||||
setEditDialogOpen,
|
||||
setGroupDialogOpen,
|
||||
setViewDialogOpen,
|
||||
setEdgeDialogOpen,
|
||||
handleSaveIdea,
|
||||
handleDeleteIdea,
|
||||
handleSaveGroup,
|
||||
handleDeleteGroup,
|
||||
handleDeleteEdge,
|
||||
handleSaveEdge,
|
||||
}: FeatureIdeaDialogsProps) => (
|
||||
<>
|
||||
<GroupDialog
|
||||
open={groupDialogOpen}
|
||||
onOpenChange={setGroupDialogOpen}
|
||||
selectedGroup={selectedGroup}
|
||||
groups={groups}
|
||||
setSelectedGroup={setSelectedGroup}
|
||||
onSave={handleSaveGroup}
|
||||
onDelete={handleDeleteGroup}
|
||||
/>
|
||||
|
||||
<IdeaEditDialog
|
||||
open={editDialogOpen}
|
||||
onOpenChange={setEditDialogOpen}
|
||||
selectedIdea={selectedIdea}
|
||||
ideas={ideas}
|
||||
safeGroups={safeGroups}
|
||||
setSelectedIdea={setSelectedIdea}
|
||||
onSave={handleSaveIdea}
|
||||
onDelete={handleDeleteIdea}
|
||||
/>
|
||||
|
||||
<IdeaViewDialog
|
||||
open={viewDialogOpen}
|
||||
onOpenChange={setViewDialogOpen}
|
||||
selectedIdea={selectedIdea}
|
||||
safeGroups={safeGroups}
|
||||
safeIdeas={safeIdeas}
|
||||
edges={edges}
|
||||
onEdit={() => {
|
||||
setViewDialogOpen(false)
|
||||
setEditDialogOpen(true)
|
||||
}}
|
||||
/>
|
||||
|
||||
<EdgeDialog
|
||||
open={edgeDialogOpen}
|
||||
onOpenChange={setEdgeDialogOpen}
|
||||
selectedEdge={selectedEdge}
|
||||
safeIdeas={safeIdeas}
|
||||
setSelectedEdge={setSelectedEdge}
|
||||
onDelete={handleDeleteEdge}
|
||||
onSave={handleSaveEdge}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
15
src/components/FeatureIdeaCloud/FeatureIdeaTipsPanel.tsx
Normal file
15
src/components/FeatureIdeaCloud/FeatureIdeaTipsPanel.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Panel } from 'reactflow'
|
||||
|
||||
export const FeatureIdeaTipsPanel = () => (
|
||||
<Panel position="bottom-right">
|
||||
<div className="bg-card border border-border rounded-lg shadow-lg p-2 text-xs text-muted-foreground max-w-sm">
|
||||
<p className="mb-1">
|
||||
💡 <strong>Tip:</strong> Double-click ideas to view details
|
||||
</p>
|
||||
<p className="mb-1">📦 Create groups to organize related ideas</p>
|
||||
<p className="mb-1">🔗 Drag from handles on card edges to connect ideas</p>
|
||||
<p className="mb-1">↪️ Drag existing connection ends to remap them</p>
|
||||
<p>⚙️ Click connections to edit or delete them</p>
|
||||
</div>
|
||||
</Panel>
|
||||
)
|
||||
65
src/components/FeatureIdeaCloud/FeatureIdeaToolbar.tsx
Normal file
65
src/components/FeatureIdeaCloud/FeatureIdeaToolbar.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Panel } from 'reactflow'
|
||||
import { Plus, Package, Sparkle } from '@phosphor-icons/react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
|
||||
type FeatureIdeaToolbarProps = {
|
||||
debugPanelOpen: boolean
|
||||
setDebugPanelOpen: (open: boolean) => void
|
||||
handleGenerateIdeas: () => void
|
||||
handleAddGroup: () => void
|
||||
handleAddIdea: () => void
|
||||
}
|
||||
|
||||
export const FeatureIdeaToolbar = ({
|
||||
debugPanelOpen,
|
||||
setDebugPanelOpen,
|
||||
handleGenerateIdeas,
|
||||
handleAddGroup,
|
||||
handleAddIdea,
|
||||
}: FeatureIdeaToolbarProps) => (
|
||||
<Panel position="top-right" className="flex gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={() => setDebugPanelOpen(!debugPanelOpen)}
|
||||
variant="outline"
|
||||
className="shadow-lg"
|
||||
size="icon"
|
||||
>
|
||||
🔍
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Debug Connection Status</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button onClick={handleGenerateIdeas} variant="outline" className="shadow-lg">
|
||||
<Sparkle size={20} weight="duotone" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>AI Generate Ideas</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button onClick={handleAddGroup} variant="outline" className="shadow-lg">
|
||||
<Package size={20} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add Group</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button onClick={handleAddIdea} className="shadow-lg">
|
||||
<Plus size={20} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add Idea</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Panel>
|
||||
)
|
||||
@@ -1,98 +1,3 @@
|
||||
import { FeatureIdea } from './types'
|
||||
|
||||
export const SEED_IDEAS: FeatureIdea[] = [
|
||||
{
|
||||
id: 'idea-1',
|
||||
title: 'AI Code Assistant',
|
||||
description: 'Integrate an AI assistant that can suggest code improvements and answer questions',
|
||||
category: 'AI/ML',
|
||||
priority: 'high',
|
||||
status: 'completed',
|
||||
createdAt: Date.now() - 10000000,
|
||||
},
|
||||
{
|
||||
id: 'idea-2',
|
||||
title: 'Real-time Collaboration',
|
||||
description: 'Allow multiple developers to work on the same project simultaneously',
|
||||
category: 'Collaboration',
|
||||
priority: 'high',
|
||||
status: 'idea',
|
||||
createdAt: Date.now() - 9000000,
|
||||
},
|
||||
{
|
||||
id: 'idea-3',
|
||||
title: 'Component Marketplace',
|
||||
description: 'A marketplace where users can share and download pre-built components',
|
||||
category: 'Community',
|
||||
priority: 'medium',
|
||||
status: 'idea',
|
||||
createdAt: Date.now() - 8000000,
|
||||
},
|
||||
{
|
||||
id: 'idea-4',
|
||||
title: 'Visual Git Integration',
|
||||
description: 'Git operations through a visual interface with branch visualization',
|
||||
category: 'DevOps',
|
||||
priority: 'high',
|
||||
status: 'planned',
|
||||
createdAt: Date.now() - 7000000,
|
||||
},
|
||||
{
|
||||
id: 'idea-5',
|
||||
title: 'API Mock Server',
|
||||
description: 'Built-in mock server for testing API integrations',
|
||||
category: 'Testing',
|
||||
priority: 'medium',
|
||||
status: 'idea',
|
||||
createdAt: Date.now() - 6000000,
|
||||
},
|
||||
{
|
||||
id: 'idea-6',
|
||||
title: 'Performance Profiler',
|
||||
description: 'Analyze and optimize application performance with visual metrics',
|
||||
category: 'Performance',
|
||||
priority: 'medium',
|
||||
status: 'idea',
|
||||
createdAt: Date.now() - 5000000,
|
||||
},
|
||||
{
|
||||
id: 'idea-7',
|
||||
title: 'Theme Presets',
|
||||
description: 'Pre-designed theme templates for quick project setup',
|
||||
category: 'Design',
|
||||
priority: 'low',
|
||||
status: 'completed',
|
||||
createdAt: Date.now() - 4000000,
|
||||
},
|
||||
{
|
||||
id: 'idea-8',
|
||||
title: 'Database Schema Migrations',
|
||||
description: 'Visual tool for creating and managing database migrations',
|
||||
category: 'Database',
|
||||
priority: 'high',
|
||||
status: 'in-progress',
|
||||
createdAt: Date.now() - 3000000,
|
||||
},
|
||||
{
|
||||
id: 'idea-9',
|
||||
title: 'Mobile App Preview',
|
||||
description: 'Live preview on actual mobile devices or simulators',
|
||||
category: 'Mobile',
|
||||
priority: 'medium',
|
||||
status: 'planned',
|
||||
createdAt: Date.now() - 2000000,
|
||||
},
|
||||
{
|
||||
id: 'idea-10',
|
||||
title: 'Accessibility Checker',
|
||||
description: 'Automated accessibility testing and suggestions',
|
||||
category: 'Accessibility',
|
||||
priority: 'high',
|
||||
status: 'idea',
|
||||
createdAt: Date.now() - 1000000,
|
||||
},
|
||||
]
|
||||
|
||||
export const CONNECTION_STYLE = {
|
||||
stroke: '#a78bfa',
|
||||
strokeWidth: 2.5
|
||||
|
||||
90
src/components/FeatureIdeaCloud/dialogs/EdgeDialog.tsx
Normal file
90
src/components/FeatureIdeaCloud/dialogs/EdgeDialog.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Trash } from '@phosphor-icons/react'
|
||||
import { Edge } from 'reactflow'
|
||||
import { FeatureIdea, IdeaEdgeData } from '../types'
|
||||
|
||||
type EdgeDialogProps = {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
selectedEdge: Edge<IdeaEdgeData> | null
|
||||
safeIdeas: FeatureIdea[]
|
||||
setSelectedEdge: (edge: Edge<IdeaEdgeData> | null) => void
|
||||
onDelete: (edgeId: string) => void
|
||||
onSave: () => void
|
||||
}
|
||||
|
||||
export const EdgeDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
selectedEdge,
|
||||
safeIdeas,
|
||||
setSelectedEdge,
|
||||
onDelete,
|
||||
onSave,
|
||||
}: EdgeDialogProps) => (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Connection Details</DialogTitle>
|
||||
<DialogDescription>Manage the relationship between ideas</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedEdge && selectedEdge.data && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground">From</label>
|
||||
<p className="text-sm font-medium">{safeIdeas.find((idea) => idea.id === selectedEdge.source)?.title}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground">To</label>
|
||||
<p className="text-sm font-medium">{safeIdeas.find((idea) => idea.id === selectedEdge.target)?.title}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Label</label>
|
||||
<Input
|
||||
value={selectedEdge.data?.label || ''}
|
||||
onChange={(e) =>
|
||||
setSelectedEdge({
|
||||
...selectedEdge,
|
||||
data: {
|
||||
...selectedEdge.data!,
|
||||
label: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="relates to"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-muted rounded-lg text-sm">
|
||||
<p className="font-medium mb-2">💡 Connection Info:</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Each connection point can have exactly one arrow. Creating a new connection from an occupied point will
|
||||
automatically remap the existing connection.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex justify-between w-full">
|
||||
<Button variant="destructive" onClick={() => selectedEdge && onDelete(selectedEdge.id)}>
|
||||
<Trash size={16} className="mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onSave}>Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
93
src/components/FeatureIdeaCloud/dialogs/GroupDialog.tsx
Normal file
93
src/components/FeatureIdeaCloud/dialogs/GroupDialog.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Trash } from '@phosphor-icons/react'
|
||||
import { GROUP_COLORS } from '../constants'
|
||||
import { IdeaGroup } from '../types'
|
||||
|
||||
type GroupDialogProps = {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
selectedGroup: IdeaGroup | null
|
||||
groups: IdeaGroup[] | null
|
||||
setSelectedGroup: (group: IdeaGroup | null) => void
|
||||
onSave: () => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
export const GroupDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
selectedGroup,
|
||||
groups,
|
||||
setSelectedGroup,
|
||||
onSave,
|
||||
onDelete,
|
||||
}: GroupDialogProps) => (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedGroup?.label ? 'Edit Group' : 'New Group'}</DialogTitle>
|
||||
<DialogDescription>Create a container to organize related ideas</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedGroup && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Group Name</label>
|
||||
<Input
|
||||
value={selectedGroup.label}
|
||||
onChange={(e) => setSelectedGroup({ ...selectedGroup, label: e.target.value })}
|
||||
placeholder="e.g., Authentication Features"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Color</label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{GROUP_COLORS.map((color) => (
|
||||
<button
|
||||
key={color.value}
|
||||
onClick={() => setSelectedGroup({ ...selectedGroup, color: color.value })}
|
||||
className={`h-12 rounded-lg border-2 transition-all hover:scale-105 ${
|
||||
selectedGroup.color === color.value ? 'border-foreground ring-2 ring-primary' : 'border-border'
|
||||
}`}
|
||||
style={{ backgroundColor: color.value }}
|
||||
title={color.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-muted rounded-lg text-sm">
|
||||
<p className="font-medium mb-1">💡 Tips:</p>
|
||||
<ul className="space-y-1 text-xs text-muted-foreground">
|
||||
<li>• Groups provide visual organization for related ideas</li>
|
||||
<li>• Drag ideas into groups or assign them in the idea editor</li>
|
||||
<li>• Ideas stay within their group boundaries when moved</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex justify-between w-full">
|
||||
<div>
|
||||
{selectedGroup && (groups || []).find((group) => group.id === selectedGroup.id) && (
|
||||
<Button variant="destructive" onClick={() => onDelete(selectedGroup.id)}>
|
||||
<Trash size={16} className="mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onSave}>Save Group</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
143
src/components/FeatureIdeaCloud/dialogs/IdeaEditDialog.tsx
Normal file
143
src/components/FeatureIdeaCloud/dialogs/IdeaEditDialog.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Trash } from '@phosphor-icons/react'
|
||||
import { CATEGORIES, PRIORITIES, STATUSES } from '../constants'
|
||||
import { FeatureIdea, IdeaGroup } from '../types'
|
||||
|
||||
type IdeaEditDialogProps = {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
selectedIdea: FeatureIdea | null
|
||||
ideas: FeatureIdea[] | null
|
||||
safeGroups: IdeaGroup[]
|
||||
setSelectedIdea: (idea: FeatureIdea | null) => void
|
||||
onSave: () => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
export const IdeaEditDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
selectedIdea,
|
||||
ideas,
|
||||
safeGroups,
|
||||
setSelectedIdea,
|
||||
onSave,
|
||||
onDelete,
|
||||
}: IdeaEditDialogProps) => (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedIdea?.title ? 'Edit Idea' : 'New Idea'}</DialogTitle>
|
||||
<DialogDescription>Create or modify a feature idea for your app</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedIdea && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Title</label>
|
||||
<Input
|
||||
value={selectedIdea.title}
|
||||
onChange={(e) => setSelectedIdea({ ...selectedIdea, title: e.target.value })}
|
||||
placeholder="Feature name..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Description</label>
|
||||
<Textarea
|
||||
value={selectedIdea.description}
|
||||
onChange={(e) => setSelectedIdea({ ...selectedIdea, description: e.target.value })}
|
||||
placeholder="Describe the feature..."
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Category</label>
|
||||
<select
|
||||
value={selectedIdea.category}
|
||||
onChange={(e) => setSelectedIdea({ ...selectedIdea, category: e.target.value })}
|
||||
className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm"
|
||||
>
|
||||
{CATEGORIES.map((category) => (
|
||||
<option key={category} value={category}>
|
||||
{category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Priority</label>
|
||||
<select
|
||||
value={selectedIdea.priority}
|
||||
onChange={(e) => setSelectedIdea({ ...selectedIdea, priority: e.target.value as FeatureIdea['priority'] })}
|
||||
className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm"
|
||||
>
|
||||
{PRIORITIES.map((priority) => (
|
||||
<option key={priority} value={priority}>
|
||||
{priority.charAt(0).toUpperCase() + priority.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Status</label>
|
||||
<select
|
||||
value={selectedIdea.status}
|
||||
onChange={(e) => setSelectedIdea({ ...selectedIdea, status: e.target.value as FeatureIdea['status'] })}
|
||||
className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm"
|
||||
>
|
||||
{STATUSES.map((status) => (
|
||||
<option key={status} value={status}>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1).replace('-', ' ')}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Group</label>
|
||||
<select
|
||||
value={selectedIdea.parentGroup || ''}
|
||||
onChange={(e) => setSelectedIdea({ ...selectedIdea, parentGroup: e.target.value || undefined })}
|
||||
className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm"
|
||||
>
|
||||
<option value="">No group</option>
|
||||
{safeGroups.map((group) => (
|
||||
<option key={group.id} value={group.id}>
|
||||
{group.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex justify-between w-full">
|
||||
<div>
|
||||
{selectedIdea && (ideas || []).find((idea) => idea.id === selectedIdea.id) && (
|
||||
<Button variant="destructive" onClick={() => onDelete(selectedIdea.id)}>
|
||||
<Trash size={16} className="mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onSave}>Save Idea</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
105
src/components/FeatureIdeaCloud/dialogs/IdeaViewDialog.tsx
Normal file
105
src/components/FeatureIdeaCloud/dialogs/IdeaViewDialog.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { STATUS_COLORS } from '../constants'
|
||||
import { FeatureIdea, IdeaGroup, IdeaEdgeData } from '../types'
|
||||
import { Edge } from 'reactflow'
|
||||
|
||||
type IdeaViewDialogProps = {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
selectedIdea: FeatureIdea | null
|
||||
safeGroups: IdeaGroup[]
|
||||
safeIdeas: FeatureIdea[]
|
||||
edges: Edge<IdeaEdgeData>[]
|
||||
onEdit: () => void
|
||||
}
|
||||
|
||||
export const IdeaViewDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
selectedIdea,
|
||||
safeGroups,
|
||||
safeIdeas,
|
||||
edges,
|
||||
onEdit,
|
||||
}: IdeaViewDialogProps) => (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedIdea?.title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedIdea && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">{selectedIdea.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground">Category</label>
|
||||
<p className="text-sm font-medium">{selectedIdea.category}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground">Priority</label>
|
||||
<p className="text-sm font-medium capitalize">{selectedIdea.priority}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground">Status</label>
|
||||
<Badge className={`${STATUS_COLORS[selectedIdea.status]} mt-1`}>{selectedIdea.status}</Badge>
|
||||
</div>
|
||||
|
||||
{selectedIdea.parentGroup && (
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground">Group</label>
|
||||
<p className="text-sm font-medium">
|
||||
{safeGroups.find((group) => group.id === selectedIdea.parentGroup)?.label || 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground">Created</label>
|
||||
<p className="text-sm">{new Date(selectedIdea.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground mb-2 block">Connections</label>
|
||||
<div className="space-y-2">
|
||||
{edges
|
||||
.filter((edge) => edge.source === selectedIdea.id || edge.target === selectedIdea.id)
|
||||
.map((edge) => {
|
||||
const otherIdeaId = edge.source === selectedIdea.id ? edge.target : edge.source
|
||||
const otherIdea = safeIdeas.find((idea) => idea.id === otherIdeaId)
|
||||
const isOutgoing = edge.source === selectedIdea.id
|
||||
return (
|
||||
<div key={edge.id} className="flex items-center gap-2 text-sm p-2 bg-muted rounded">
|
||||
<span className="flex-1">{isOutgoing ? '→' : '←'} {otherIdea?.title || 'Unknown'}</span>
|
||||
{edge.data?.label && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{edge.data.label}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{edges.filter((edge) => edge.source === selectedIdea.id || edge.target === selectedIdea.id).length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">No connections</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={onEdit}>Edit</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
45
src/components/FeatureIdeaCloud/seed-data.ts
Normal file
45
src/components/FeatureIdeaCloud/seed-data.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Edge, MarkerType } from 'reactflow'
|
||||
import seedData from '@/data/feature-idea-cloud.json'
|
||||
import { FeatureIdea, IdeaEdgeData } from './types'
|
||||
import { CONNECTION_STYLE } from './constants'
|
||||
|
||||
type SeedIdea = Omit<FeatureIdea, 'createdAt'> & { createdAtOffset: number }
|
||||
|
||||
type SeedEdge = {
|
||||
id: string
|
||||
source: string
|
||||
target: string
|
||||
sourceHandle?: string
|
||||
targetHandle?: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export const buildSeedIdeas = (): FeatureIdea[] => {
|
||||
const now = Date.now()
|
||||
return (seedData.ideas as SeedIdea[]).map((idea) => ({
|
||||
...idea,
|
||||
createdAt: now + idea.createdAtOffset,
|
||||
}))
|
||||
}
|
||||
|
||||
export const buildSeedEdges = (): Edge<IdeaEdgeData>[] =>
|
||||
(seedData.edges as SeedEdge[]).map((edge) => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
sourceHandle: edge.sourceHandle,
|
||||
targetHandle: edge.targetHandle,
|
||||
type: 'default',
|
||||
animated: false,
|
||||
data: { label: edge.label },
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: CONNECTION_STYLE.stroke,
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
style: {
|
||||
stroke: CONNECTION_STYLE.stroke,
|
||||
strokeWidth: CONNECTION_STYLE.strokeWidth,
|
||||
},
|
||||
}))
|
||||
722
src/components/FeatureIdeaCloud/useFeatureIdeaCloud.ts
Normal file
722
src/components/FeatureIdeaCloud/useFeatureIdeaCloud.ts
Normal file
@@ -0,0 +1,722 @@
|
||||
import { MouseEvent as ReactMouseEvent, useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { Edge, Node, useNodesState, useEdgesState, Connection as RFConnection, MarkerType, reconnectEdge } from 'reactflow'
|
||||
import { toast } from 'sonner'
|
||||
import { useKV } from '@/hooks/use-kv'
|
||||
import { FeatureIdea, IdeaGroup, IdeaEdgeData } from './types'
|
||||
import { CATEGORIES, CONNECTION_STYLE, GROUP_COLORS } from './constants'
|
||||
import { buildSeedEdges, buildSeedIdeas } from './seed-data'
|
||||
|
||||
type SeedRef = {
|
||||
ideas: FeatureIdea[]
|
||||
edges: Edge<IdeaEdgeData>[]
|
||||
}
|
||||
|
||||
export type FeatureIdeaCloudState = {
|
||||
nodes: Node[]
|
||||
edges: Edge<IdeaEdgeData>[]
|
||||
ideas: FeatureIdea[] | null
|
||||
groups: IdeaGroup[] | null
|
||||
safeIdeas: FeatureIdea[]
|
||||
safeGroups: IdeaGroup[]
|
||||
selectedIdea: FeatureIdea | null
|
||||
selectedGroup: IdeaGroup | null
|
||||
selectedEdge: Edge<IdeaEdgeData> | null
|
||||
editDialogOpen: boolean
|
||||
groupDialogOpen: boolean
|
||||
viewDialogOpen: boolean
|
||||
edgeDialogOpen: boolean
|
||||
debugPanelOpen: boolean
|
||||
setSelectedIdea: (idea: FeatureIdea | null) => void
|
||||
setSelectedGroup: (group: IdeaGroup | null) => void
|
||||
setSelectedEdge: (edge: Edge<IdeaEdgeData> | null) => void
|
||||
setEditDialogOpen: (open: boolean) => void
|
||||
setGroupDialogOpen: (open: boolean) => void
|
||||
setViewDialogOpen: (open: boolean) => void
|
||||
setEdgeDialogOpen: (open: boolean) => void
|
||||
setDebugPanelOpen: (open: boolean) => void
|
||||
onNodesChangeWrapper: (changes: any) => void
|
||||
onEdgesChangeWrapper: (changes: any) => void
|
||||
onConnect: (params: RFConnection) => void
|
||||
onEdgeClick: (event: ReactMouseEvent, edge: Edge<IdeaEdgeData>) => void
|
||||
onNodeDoubleClick: (event: ReactMouseEvent, node: Node<FeatureIdea>) => void
|
||||
onReconnectStart: () => void
|
||||
onReconnect: (oldEdge: Edge, newConnection: RFConnection) => void
|
||||
onReconnectEnd: (_: MouseEvent | TouchEvent, edge: Edge) => void
|
||||
handleAddIdea: () => void
|
||||
handleAddGroup: () => void
|
||||
handleSaveIdea: () => void
|
||||
handleDeleteIdea: (id: string) => void
|
||||
handleSaveGroup: () => void
|
||||
handleDeleteGroup: (id: string) => void
|
||||
handleDeleteEdge: (edgeId: string) => void
|
||||
handleSaveEdge: () => void
|
||||
handleGenerateIdeas: () => Promise<void>
|
||||
}
|
||||
|
||||
export const useFeatureIdeaCloud = (): FeatureIdeaCloudState => {
|
||||
const seedRef = useRef<SeedRef | null>(null)
|
||||
if (!seedRef.current) {
|
||||
seedRef.current = { ideas: buildSeedIdeas(), edges: buildSeedEdges() }
|
||||
}
|
||||
const seedIdeas = seedRef.current.ideas
|
||||
const seedEdges = seedRef.current.edges
|
||||
|
||||
const [ideas, setIdeas] = useKV<FeatureIdea[]>('feature-ideas', seedIdeas)
|
||||
const [groups, setGroups] = useKV<IdeaGroup[]>('feature-idea-groups', [])
|
||||
const [savedEdges, setSavedEdges] = useKV<Edge<IdeaEdgeData>[]>('feature-idea-edges', seedEdges)
|
||||
const [savedNodePositions, setSavedNodePositions] = useKV<Record<string, { x: number; y: number }>>('feature-idea-node-positions', {})
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([])
|
||||
const [selectedIdea, setSelectedIdea] = useState<FeatureIdea | null>(null)
|
||||
const [selectedGroup, setSelectedGroup] = useState<IdeaGroup | null>(null)
|
||||
const [selectedEdge, setSelectedEdge] = useState<Edge<IdeaEdgeData> | null>(null)
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false)
|
||||
const [groupDialogOpen, setGroupDialogOpen] = useState(false)
|
||||
const [viewDialogOpen, setViewDialogOpen] = useState(false)
|
||||
const [edgeDialogOpen, setEdgeDialogOpen] = useState(false)
|
||||
const [debugPanelOpen, setDebugPanelOpen] = useState(false)
|
||||
const edgeReconnectSuccessful = useRef(true)
|
||||
|
||||
const safeIdeas = ideas || seedIdeas
|
||||
const safeGroups = groups || []
|
||||
const safeEdges = savedEdges || []
|
||||
const safeNodePositions = savedNodePositions || {}
|
||||
|
||||
const updateNodeConnectionCounts = useCallback((edgeList: Edge<IdeaEdgeData>[]) => {
|
||||
const nodeConnectionMap = new Map<string, Record<string, Set<string>>>()
|
||||
|
||||
edgeList.forEach((edge) => {
|
||||
const sourceHandle = edge.sourceHandle || 'default'
|
||||
const targetHandle = edge.targetHandle || 'default'
|
||||
|
||||
if (!nodeConnectionMap.has(edge.source)) {
|
||||
nodeConnectionMap.set(edge.source, { left: new Set(), right: new Set(), top: new Set(), bottom: new Set() })
|
||||
}
|
||||
if (!nodeConnectionMap.has(edge.target)) {
|
||||
nodeConnectionMap.set(edge.target, { left: new Set(), right: new Set(), top: new Set(), bottom: new Set() })
|
||||
}
|
||||
|
||||
const sourceSide = sourceHandle.split('-')[0]
|
||||
const targetSide = targetHandle.split('-')[0]
|
||||
|
||||
nodeConnectionMap.get(edge.source)![sourceSide].add(sourceHandle)
|
||||
nodeConnectionMap.get(edge.target)![targetSide].add(targetHandle)
|
||||
})
|
||||
|
||||
nodeConnectionMap.forEach((connections, nodeId) => {
|
||||
const counts = {
|
||||
left: connections.left.size,
|
||||
right: connections.right.size,
|
||||
top: connections.top.size,
|
||||
bottom: connections.bottom.size,
|
||||
}
|
||||
|
||||
const event = new CustomEvent('updateConnectionCounts', {
|
||||
detail: { nodeId, counts },
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!ideas || ideas.length === 0) {
|
||||
setIdeas(seedIdeas)
|
||||
}
|
||||
}, [ideas, seedIdeas, setIdeas])
|
||||
|
||||
useEffect(() => {
|
||||
const groupNodes: Node<IdeaGroup>[] = safeGroups.map((group) => ({
|
||||
id: group.id,
|
||||
type: 'groupNode',
|
||||
position: safeNodePositions[group.id] || { x: 0, y: 0 },
|
||||
data: group,
|
||||
style: {
|
||||
zIndex: -1,
|
||||
},
|
||||
}))
|
||||
|
||||
const ideaNodes: Node<FeatureIdea>[] = safeIdeas.map((idea, index) => ({
|
||||
id: idea.id,
|
||||
type: 'ideaNode',
|
||||
position: safeNodePositions[idea.id] || { x: 100 + (index % 3) * 350, y: 100 + Math.floor(index / 3) * 250 },
|
||||
data: idea,
|
||||
parentNode: idea.parentGroup,
|
||||
extent: idea.parentGroup ? 'parent' : undefined,
|
||||
style: {
|
||||
zIndex: 1,
|
||||
},
|
||||
}))
|
||||
|
||||
setNodes([...groupNodes, ...ideaNodes])
|
||||
}, [safeIdeas, safeGroups, safeNodePositions, setNodes])
|
||||
|
||||
useEffect(() => {
|
||||
setEdges(safeEdges)
|
||||
updateNodeConnectionCounts(safeEdges)
|
||||
}, [safeEdges, setEdges, updateNodeConnectionCounts])
|
||||
|
||||
useEffect(() => {
|
||||
const handleEditIdea = (e: Event) => {
|
||||
const customEvent = e as CustomEvent<FeatureIdea>
|
||||
setSelectedIdea(customEvent.detail)
|
||||
setEditDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleEditGroup = (e: Event) => {
|
||||
const customEvent = e as CustomEvent<IdeaGroup>
|
||||
setSelectedGroup(customEvent.detail)
|
||||
setGroupDialogOpen(true)
|
||||
}
|
||||
|
||||
window.addEventListener('editIdea', handleEditIdea)
|
||||
window.addEventListener('editGroup', handleEditGroup)
|
||||
return () => {
|
||||
window.removeEventListener('editIdea', handleEditIdea)
|
||||
window.removeEventListener('editGroup', handleEditGroup)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onNodesChangeWrapper = useCallback(
|
||||
(changes: any) => {
|
||||
onNodesChange(changes)
|
||||
const moveChange = changes.find((c: any) => c.type === 'position' && c.dragging === false)
|
||||
if (moveChange) {
|
||||
setTimeout(() => {
|
||||
setNodes((currentNodes) => {
|
||||
const positions: Record<string, { x: number; y: number }> = {}
|
||||
currentNodes.forEach((node) => {
|
||||
if (node.position) {
|
||||
positions[node.id] = node.position
|
||||
}
|
||||
})
|
||||
setSavedNodePositions(positions)
|
||||
return currentNodes
|
||||
})
|
||||
setEdges((currentEdges) => {
|
||||
setSavedEdges(currentEdges)
|
||||
return currentEdges
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
[onNodesChange, setNodes, setEdges, setSavedNodePositions, setSavedEdges]
|
||||
)
|
||||
|
||||
const onEdgesChangeWrapper = useCallback(
|
||||
(changes: any) => {
|
||||
onEdgesChange(changes)
|
||||
setTimeout(() => {
|
||||
setEdges((currentEdges) => {
|
||||
setSavedEdges(currentEdges)
|
||||
updateNodeConnectionCounts(currentEdges)
|
||||
return currentEdges
|
||||
})
|
||||
}, 100)
|
||||
},
|
||||
[onEdgesChange, setEdges, setSavedEdges, updateNodeConnectionCounts]
|
||||
)
|
||||
|
||||
const validateAndRemoveConflicts = useCallback(
|
||||
(
|
||||
edgeList: Edge<IdeaEdgeData>[],
|
||||
sourceNodeId: string,
|
||||
sourceHandleId: string,
|
||||
targetNodeId: string,
|
||||
targetHandleId: string,
|
||||
excludeEdgeId?: string
|
||||
): { filteredEdges: Edge<IdeaEdgeData>[]; removedCount: number; conflicts: string[] } => {
|
||||
const edgesToRemove: string[] = []
|
||||
const conflicts: string[] = []
|
||||
|
||||
console.log('[Validator] Checking for conflicts:', {
|
||||
newConnection: `${sourceNodeId}[${sourceHandleId}] -> ${targetNodeId}[${targetHandleId}]`,
|
||||
existingEdges: edgeList.length,
|
||||
excludeEdgeId,
|
||||
})
|
||||
|
||||
edgeList.forEach((edge) => {
|
||||
if (excludeEdgeId && edge.id === excludeEdgeId) {
|
||||
console.log('[Validator] Skipping excluded edge:', edge.id)
|
||||
return
|
||||
}
|
||||
|
||||
const edgeSourceHandle = edge.sourceHandle || 'default'
|
||||
const edgeTargetHandle = edge.targetHandle || 'default'
|
||||
|
||||
const hasSourceConflict = edge.source === sourceNodeId && edgeSourceHandle === sourceHandleId
|
||||
const hasTargetConflict = edge.target === targetNodeId && edgeTargetHandle === targetHandleId
|
||||
|
||||
if (hasSourceConflict && !edgesToRemove.includes(edge.id)) {
|
||||
edgesToRemove.push(edge.id)
|
||||
conflicts.push(`Source: ${edge.source}[${edgeSourceHandle}] was connected to ${edge.target}[${edgeTargetHandle}]`)
|
||||
console.log('[Validator] SOURCE CONFLICT DETECTED:', edge.id, edge)
|
||||
}
|
||||
|
||||
if (hasTargetConflict && !edgesToRemove.includes(edge.id)) {
|
||||
edgesToRemove.push(edge.id)
|
||||
conflicts.push(`Target: ${edge.target}[${edgeTargetHandle}] was connected from ${edge.source}[${edgeSourceHandle}]`)
|
||||
console.log('[Validator] TARGET CONFLICT DETECTED:', edge.id, edge)
|
||||
}
|
||||
})
|
||||
|
||||
const filteredEdges = edgeList.filter((edge) => !edgesToRemove.includes(edge.id))
|
||||
|
||||
console.log('[Validator] Conflicts found:', conflicts.length, 'edges to remove:', edgesToRemove)
|
||||
|
||||
return {
|
||||
filteredEdges,
|
||||
removedCount: edgesToRemove.length,
|
||||
conflicts,
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params: RFConnection) => {
|
||||
if (!params.source || !params.target) return
|
||||
|
||||
const sourceNodeId = params.source
|
||||
const sourceHandleId = params.sourceHandle || 'default'
|
||||
const targetNodeId = params.target
|
||||
const targetHandleId = params.targetHandle || 'default'
|
||||
|
||||
console.log('[Connection] ==== NEW CONNECTION ATTEMPT ====')
|
||||
console.log('[Connection] Source:', `${sourceNodeId}[${sourceHandleId}]`)
|
||||
console.log('[Connection] Target:', `${targetNodeId}[${targetHandleId}]`)
|
||||
|
||||
setEdges((eds) => {
|
||||
console.log('[Connection] Current edges BEFORE validation:', eds.length)
|
||||
eds.forEach((edge) => {
|
||||
console.log(` - ${edge.id}: ${edge.source}[${edge.sourceHandle || 'default'}] -> ${edge.target}[${edge.targetHandle || 'default'}]`)
|
||||
})
|
||||
|
||||
const { filteredEdges, removedCount, conflicts } = validateAndRemoveConflicts(
|
||||
eds,
|
||||
sourceNodeId,
|
||||
sourceHandleId,
|
||||
targetNodeId,
|
||||
targetHandleId
|
||||
)
|
||||
|
||||
console.log('[Connection] Edges AFTER conflict removal:', filteredEdges.length)
|
||||
|
||||
const newEdge: Edge<IdeaEdgeData> = {
|
||||
id: `edge-${Date.now()}`,
|
||||
source: sourceNodeId,
|
||||
target: targetNodeId,
|
||||
sourceHandle: sourceHandleId,
|
||||
targetHandle: targetHandleId,
|
||||
type: 'default',
|
||||
data: { label: 'relates to' },
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: CONNECTION_STYLE.stroke,
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
style: {
|
||||
stroke: CONNECTION_STYLE.stroke,
|
||||
strokeWidth: CONNECTION_STYLE.strokeWidth,
|
||||
},
|
||||
animated: false,
|
||||
}
|
||||
|
||||
console.log('[Connection] Creating new edge:', newEdge.id)
|
||||
|
||||
const updatedEdges = [...filteredEdges, newEdge]
|
||||
|
||||
console.log('[Connection] Total edges AFTER addition:', updatedEdges.length)
|
||||
console.log('[Connection] Final edge list:')
|
||||
updatedEdges.forEach((edge) => {
|
||||
console.log(` - ${edge.id}: ${edge.source}[${edge.sourceHandle || 'default'}] -> ${edge.target}[${edge.targetHandle || 'default'}]`)
|
||||
})
|
||||
|
||||
setSavedEdges(updatedEdges)
|
||||
updateNodeConnectionCounts(updatedEdges)
|
||||
|
||||
if (removedCount > 0) {
|
||||
setTimeout(() => {
|
||||
toast.success(`Connection remapped! (${removedCount} old connection${removedCount > 1 ? 's' : ''} removed)`, {
|
||||
description: conflicts.join('\n'),
|
||||
})
|
||||
}, 0)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
toast.success('Ideas connected!')
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return updatedEdges
|
||||
})
|
||||
},
|
||||
[setEdges, setSavedEdges, validateAndRemoveConflicts, updateNodeConnectionCounts]
|
||||
)
|
||||
|
||||
const onEdgeClick = useCallback((event: ReactMouseEvent, edge: Edge<IdeaEdgeData>) => {
|
||||
setSelectedEdge(edge)
|
||||
setEdgeDialogOpen(true)
|
||||
}, [])
|
||||
|
||||
const onNodeDoubleClick = useCallback((event: ReactMouseEvent, node: Node<FeatureIdea>) => {
|
||||
setSelectedIdea(node.data)
|
||||
setViewDialogOpen(true)
|
||||
}, [])
|
||||
|
||||
const onReconnectStart = useCallback(() => {
|
||||
edgeReconnectSuccessful.current = false
|
||||
}, [])
|
||||
|
||||
const onReconnect = useCallback(
|
||||
(oldEdge: Edge, newConnection: RFConnection) => {
|
||||
if (!newConnection.source || !newConnection.target) return
|
||||
|
||||
const sourceNodeId = newConnection.source
|
||||
const sourceHandleId = newConnection.sourceHandle || 'default'
|
||||
const targetNodeId = newConnection.target
|
||||
const targetHandleId = newConnection.targetHandle || 'default'
|
||||
|
||||
console.log('[Reconnection] Remapping edge:', {
|
||||
oldEdgeId: oldEdge.id,
|
||||
oldSource: `${oldEdge.source}[${oldEdge.sourceHandle || 'default'}]`,
|
||||
oldTarget: `${oldEdge.target}[${oldEdge.targetHandle || 'default'}]`,
|
||||
newSource: `${sourceNodeId}[${sourceHandleId}]`,
|
||||
newTarget: `${targetNodeId}[${targetHandleId}]`,
|
||||
})
|
||||
|
||||
edgeReconnectSuccessful.current = true
|
||||
|
||||
setEdges((els) => {
|
||||
const { filteredEdges, removedCount, conflicts } = validateAndRemoveConflicts(
|
||||
els,
|
||||
sourceNodeId,
|
||||
sourceHandleId,
|
||||
targetNodeId,
|
||||
targetHandleId,
|
||||
oldEdge.id
|
||||
)
|
||||
|
||||
const updatedEdges = reconnectEdge(oldEdge, newConnection, filteredEdges)
|
||||
|
||||
console.log('[Reconnection] Edge remapped successfully')
|
||||
console.log('[Reconnection] Total edges after remapping:', updatedEdges.length)
|
||||
console.log('[Reconnection] Edges by handle:', updatedEdges.map((edge) => ({
|
||||
id: edge.id,
|
||||
source: `${edge.source}[${edge.sourceHandle || 'default'}]`,
|
||||
target: `${edge.target}[${edge.targetHandle || 'default'}]`,
|
||||
})))
|
||||
|
||||
setSavedEdges(updatedEdges)
|
||||
updateNodeConnectionCounts(updatedEdges)
|
||||
|
||||
if (removedCount > 0) {
|
||||
setTimeout(() => {
|
||||
toast.success(
|
||||
`Connection remapped! (${removedCount} conflicting connection${removedCount > 1 ? 's' : ''} removed)`,
|
||||
{
|
||||
description: conflicts.join('\n'),
|
||||
}
|
||||
)
|
||||
}, 0)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
toast.success('Connection remapped!')
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return updatedEdges
|
||||
})
|
||||
},
|
||||
[setEdges, setSavedEdges, validateAndRemoveConflicts, updateNodeConnectionCounts]
|
||||
)
|
||||
|
||||
const onReconnectEnd = useCallback(
|
||||
(_: MouseEvent | TouchEvent, edge: Edge) => {
|
||||
if (!edgeReconnectSuccessful.current) {
|
||||
setEdges((eds) => {
|
||||
const updatedEdges = eds.filter((current) => current.id !== edge.id)
|
||||
setSavedEdges(updatedEdges)
|
||||
return updatedEdges
|
||||
})
|
||||
}
|
||||
edgeReconnectSuccessful.current = true
|
||||
},
|
||||
[setEdges, setSavedEdges]
|
||||
)
|
||||
|
||||
const handleAddIdea = () => {
|
||||
const newIdea: FeatureIdea = {
|
||||
id: `idea-${Date.now()}`,
|
||||
title: '',
|
||||
description: '',
|
||||
category: 'Other',
|
||||
priority: 'medium',
|
||||
status: 'idea',
|
||||
createdAt: Date.now(),
|
||||
}
|
||||
setSelectedIdea(newIdea)
|
||||
setEditDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleAddGroup = () => {
|
||||
const newGroup: IdeaGroup = {
|
||||
id: `group-${Date.now()}`,
|
||||
label: '',
|
||||
color: GROUP_COLORS[0].value,
|
||||
createdAt: Date.now(),
|
||||
}
|
||||
setSelectedGroup(newGroup)
|
||||
setGroupDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleSaveIdea = () => {
|
||||
if (!selectedIdea || !selectedIdea.title.trim()) {
|
||||
toast.error('Please enter a title')
|
||||
return
|
||||
}
|
||||
|
||||
setIdeas((currentIdeas) => {
|
||||
const existing = (currentIdeas || []).find((idea) => idea.id === selectedIdea.id)
|
||||
if (existing) {
|
||||
return (currentIdeas || []).map((idea) => (idea.id === selectedIdea.id ? selectedIdea : idea))
|
||||
}
|
||||
return [...(currentIdeas || []), selectedIdea]
|
||||
})
|
||||
|
||||
if (!(ideas || []).find((idea) => idea.id === selectedIdea.id)) {
|
||||
const newPosition = { x: 400, y: 300 }
|
||||
const newNode: Node<FeatureIdea> = {
|
||||
id: selectedIdea.id,
|
||||
type: 'ideaNode',
|
||||
position: newPosition,
|
||||
data: selectedIdea,
|
||||
}
|
||||
setNodes((nds) => [...nds, newNode])
|
||||
|
||||
setSavedNodePositions((currentPositions) => ({
|
||||
...(currentPositions || {}),
|
||||
[selectedIdea.id]: newPosition,
|
||||
}))
|
||||
}
|
||||
|
||||
setEditDialogOpen(false)
|
||||
setSelectedIdea(null)
|
||||
toast.success('Idea saved!')
|
||||
}
|
||||
|
||||
const handleDeleteIdea = (id: string) => {
|
||||
setIdeas((currentIdeas) => (currentIdeas || []).filter((idea) => idea.id !== id))
|
||||
setNodes((nds) => nds.filter((node) => node.id !== id))
|
||||
|
||||
setSavedNodePositions((currentPositions) => {
|
||||
const newPositions = { ...(currentPositions || {}) }
|
||||
delete newPositions[id]
|
||||
return newPositions
|
||||
})
|
||||
|
||||
const updatedEdges = edges.filter((edge) => edge.source !== id && edge.target !== id)
|
||||
setEdges(updatedEdges)
|
||||
setSavedEdges(updatedEdges)
|
||||
updateNodeConnectionCounts(updatedEdges)
|
||||
|
||||
setEditDialogOpen(false)
|
||||
setViewDialogOpen(false)
|
||||
setSelectedIdea(null)
|
||||
toast.success('Idea deleted')
|
||||
}
|
||||
|
||||
const handleSaveGroup = () => {
|
||||
if (!selectedGroup || !selectedGroup.label.trim()) {
|
||||
toast.error('Please enter a group name')
|
||||
return
|
||||
}
|
||||
|
||||
setGroups((currentGroups) => {
|
||||
const existing = (currentGroups || []).find((group) => group.id === selectedGroup.id)
|
||||
if (existing) {
|
||||
return (currentGroups || []).map((group) => (group.id === selectedGroup.id ? selectedGroup : group))
|
||||
}
|
||||
return [...(currentGroups || []), selectedGroup]
|
||||
})
|
||||
|
||||
if (!(groups || []).find((group) => group.id === selectedGroup.id)) {
|
||||
const newPosition = { x: 200, y: 200 }
|
||||
const newNode: Node<IdeaGroup> = {
|
||||
id: selectedGroup.id,
|
||||
type: 'groupNode',
|
||||
position: newPosition,
|
||||
data: selectedGroup,
|
||||
style: {
|
||||
zIndex: -1,
|
||||
},
|
||||
}
|
||||
setNodes((nds) => [newNode, ...nds])
|
||||
|
||||
setSavedNodePositions((currentPositions) => ({
|
||||
...(currentPositions || {}),
|
||||
[selectedGroup.id]: newPosition,
|
||||
}))
|
||||
}
|
||||
|
||||
setGroupDialogOpen(false)
|
||||
setSelectedGroup(null)
|
||||
toast.success('Group saved!')
|
||||
}
|
||||
|
||||
const handleDeleteGroup = (id: string) => {
|
||||
setIdeas((currentIdeas) =>
|
||||
(currentIdeas || []).map((idea) => (idea.parentGroup === id ? { ...idea, parentGroup: undefined } : idea))
|
||||
)
|
||||
|
||||
setGroups((currentGroups) => (currentGroups || []).filter((group) => group.id !== id))
|
||||
setNodes((nds) => nds.filter((node) => node.id !== id))
|
||||
|
||||
setSavedNodePositions((currentPositions) => {
|
||||
const newPositions = { ...(currentPositions || {}) }
|
||||
delete newPositions[id]
|
||||
return newPositions
|
||||
})
|
||||
|
||||
setGroupDialogOpen(false)
|
||||
setSelectedGroup(null)
|
||||
toast.success('Group deleted')
|
||||
}
|
||||
|
||||
const handleDeleteEdge = (edgeId: string) => {
|
||||
const updatedEdges = edges.filter((edge) => edge.id !== edgeId)
|
||||
setEdges(updatedEdges)
|
||||
setSavedEdges(updatedEdges)
|
||||
updateNodeConnectionCounts(updatedEdges)
|
||||
setEdgeDialogOpen(false)
|
||||
setSelectedEdge(null)
|
||||
toast.success('Connection removed')
|
||||
}
|
||||
|
||||
const handleSaveEdge = () => {
|
||||
if (selectedEdge) {
|
||||
const updatedEdge = {
|
||||
...selectedEdge,
|
||||
data: selectedEdge.data,
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: CONNECTION_STYLE.stroke,
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
style: {
|
||||
stroke: CONNECTION_STYLE.stroke,
|
||||
strokeWidth: CONNECTION_STYLE.strokeWidth,
|
||||
},
|
||||
animated: false,
|
||||
}
|
||||
|
||||
const updatedEdges = edges.map((edge) => (edge.id === selectedEdge.id ? updatedEdge : edge))
|
||||
setEdges(updatedEdges)
|
||||
setSavedEdges(updatedEdges)
|
||||
setEdgeDialogOpen(false)
|
||||
toast.success('Connection updated!')
|
||||
}
|
||||
}
|
||||
|
||||
const handleGenerateIdeas = async () => {
|
||||
toast.info('Generating ideas with AI...')
|
||||
|
||||
try {
|
||||
const categoryList = CATEGORIES.join('|')
|
||||
const promptText = `Generate 3 innovative feature ideas for a low-code application builder. Each idea should be practical and valuable. Return as JSON with this structure:
|
||||
{
|
||||
"ideas": [
|
||||
{
|
||||
"title": "Feature Name",
|
||||
"description": "Brief description",
|
||||
"category": "${categoryList}",
|
||||
"priority": "low|medium|high"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
const response = await window.spark.llm(promptText, 'gpt-4o-mini', true)
|
||||
const result = JSON.parse(response)
|
||||
|
||||
if (result.ideas && Array.isArray(result.ideas)) {
|
||||
const newIdeas: FeatureIdea[] = result.ideas.map((idea: any) => ({
|
||||
id: `idea-ai-${Date.now()}-${Math.random()}`,
|
||||
title: idea.title,
|
||||
description: idea.description,
|
||||
category: idea.category || 'Other',
|
||||
priority: idea.priority || 'medium',
|
||||
status: 'idea' as const,
|
||||
createdAt: Date.now(),
|
||||
}))
|
||||
|
||||
setIdeas((currentIdeas) => [...(currentIdeas || []), ...newIdeas])
|
||||
|
||||
const newPositions: Record<string, { x: number; y: number }> = {}
|
||||
const newNodes: Node<FeatureIdea>[] = newIdeas.map((idea, index) => {
|
||||
const position = { x: 400 + index * 250, y: 300 + index * 150 }
|
||||
newPositions[idea.id] = position
|
||||
return {
|
||||
id: idea.id,
|
||||
type: 'ideaNode',
|
||||
position,
|
||||
data: idea,
|
||||
}
|
||||
})
|
||||
|
||||
setNodes((nds) => [...nds, ...newNodes])
|
||||
setSavedNodePositions((currentPositions) => ({
|
||||
...(currentPositions || {}),
|
||||
...newPositions,
|
||||
}))
|
||||
|
||||
toast.success(`Generated ${newIdeas.length} new ideas!`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate ideas:', error)
|
||||
toast.error('Failed to generate ideas')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
ideas,
|
||||
groups,
|
||||
safeIdeas,
|
||||
safeGroups,
|
||||
selectedIdea,
|
||||
selectedGroup,
|
||||
selectedEdge,
|
||||
editDialogOpen,
|
||||
groupDialogOpen,
|
||||
viewDialogOpen,
|
||||
edgeDialogOpen,
|
||||
debugPanelOpen,
|
||||
setSelectedIdea,
|
||||
setSelectedGroup,
|
||||
setSelectedEdge,
|
||||
setEditDialogOpen,
|
||||
setGroupDialogOpen,
|
||||
setViewDialogOpen,
|
||||
setEdgeDialogOpen,
|
||||
setDebugPanelOpen,
|
||||
onNodesChangeWrapper,
|
||||
onEdgesChangeWrapper,
|
||||
onConnect,
|
||||
onEdgeClick,
|
||||
onNodeDoubleClick,
|
||||
onReconnectStart,
|
||||
onReconnect,
|
||||
onReconnectEnd,
|
||||
handleAddIdea,
|
||||
handleAddGroup,
|
||||
handleSaveIdea,
|
||||
handleDeleteIdea,
|
||||
handleSaveGroup,
|
||||
handleDeleteGroup,
|
||||
handleDeleteEdge,
|
||||
handleSaveEdge,
|
||||
handleGenerateIdeas,
|
||||
}
|
||||
}
|
||||
120
src/data/feature-idea-cloud.json
Normal file
120
src/data/feature-idea-cloud.json
Normal file
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"ideas": [
|
||||
{
|
||||
"id": "idea-1",
|
||||
"title": "AI Code Assistant",
|
||||
"description": "Integrate an AI assistant that can suggest code improvements and answer questions",
|
||||
"category": "AI/ML",
|
||||
"priority": "high",
|
||||
"status": "completed",
|
||||
"createdAtOffset": -10000000
|
||||
},
|
||||
{
|
||||
"id": "idea-2",
|
||||
"title": "Real-time Collaboration",
|
||||
"description": "Allow multiple developers to work on the same project simultaneously",
|
||||
"category": "Collaboration",
|
||||
"priority": "high",
|
||||
"status": "idea",
|
||||
"createdAtOffset": -9000000
|
||||
},
|
||||
{
|
||||
"id": "idea-3",
|
||||
"title": "Component Marketplace",
|
||||
"description": "A marketplace where users can share and download pre-built components",
|
||||
"category": "Community",
|
||||
"priority": "medium",
|
||||
"status": "idea",
|
||||
"createdAtOffset": -8000000
|
||||
},
|
||||
{
|
||||
"id": "idea-4",
|
||||
"title": "Visual Git Integration",
|
||||
"description": "Git operations through a visual interface with branch visualization",
|
||||
"category": "DevOps",
|
||||
"priority": "high",
|
||||
"status": "planned",
|
||||
"createdAtOffset": -7000000
|
||||
},
|
||||
{
|
||||
"id": "idea-5",
|
||||
"title": "API Mock Server",
|
||||
"description": "Built-in mock server for testing API integrations",
|
||||
"category": "Testing",
|
||||
"priority": "medium",
|
||||
"status": "idea",
|
||||
"createdAtOffset": -6000000
|
||||
},
|
||||
{
|
||||
"id": "idea-6",
|
||||
"title": "Performance Profiler",
|
||||
"description": "Analyze and optimize application performance with visual metrics",
|
||||
"category": "Performance",
|
||||
"priority": "medium",
|
||||
"status": "idea",
|
||||
"createdAtOffset": -5000000
|
||||
},
|
||||
{
|
||||
"id": "idea-7",
|
||||
"title": "Theme Presets",
|
||||
"description": "Pre-designed theme templates for quick project setup",
|
||||
"category": "Design",
|
||||
"priority": "low",
|
||||
"status": "completed",
|
||||
"createdAtOffset": -4000000
|
||||
},
|
||||
{
|
||||
"id": "idea-8",
|
||||
"title": "Database Schema Migrations",
|
||||
"description": "Visual tool for creating and managing database migrations",
|
||||
"category": "Database",
|
||||
"priority": "high",
|
||||
"status": "in-progress",
|
||||
"createdAtOffset": -3000000
|
||||
},
|
||||
{
|
||||
"id": "idea-9",
|
||||
"title": "Mobile App Preview",
|
||||
"description": "Live preview on actual mobile devices or simulators",
|
||||
"category": "Mobile",
|
||||
"priority": "medium",
|
||||
"status": "planned",
|
||||
"createdAtOffset": -2000000
|
||||
},
|
||||
{
|
||||
"id": "idea-10",
|
||||
"title": "Accessibility Checker",
|
||||
"description": "Automated accessibility testing and suggestions",
|
||||
"category": "Accessibility",
|
||||
"priority": "high",
|
||||
"status": "idea",
|
||||
"createdAtOffset": -1000000
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-1",
|
||||
"source": "idea-1",
|
||||
"target": "idea-8",
|
||||
"sourceHandle": "right-0",
|
||||
"targetHandle": "left-0",
|
||||
"label": "requires"
|
||||
},
|
||||
{
|
||||
"id": "edge-2",
|
||||
"source": "idea-2",
|
||||
"target": "idea-4",
|
||||
"sourceHandle": "bottom-0",
|
||||
"targetHandle": "top-0",
|
||||
"label": "works with"
|
||||
},
|
||||
{
|
||||
"id": "edge-3",
|
||||
"source": "idea-8",
|
||||
"target": "idea-5",
|
||||
"sourceHandle": "bottom-0",
|
||||
"targetHandle": "left-0",
|
||||
"label": "includes"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useKV } from '@/hooks/use-kv'
|
||||
import seedData from '@/data/feature-idea-cloud.json'
|
||||
|
||||
export interface FeatureIdea {
|
||||
id: string
|
||||
@@ -12,35 +13,17 @@ export interface FeatureIdea {
|
||||
parentGroup?: string
|
||||
}
|
||||
|
||||
const SEED_IDEAS: FeatureIdea[] = [
|
||||
{
|
||||
id: 'idea-1',
|
||||
title: 'AI Code Assistant',
|
||||
description: 'Integrate an AI assistant that can suggest code improvements and answer questions',
|
||||
category: 'AI/ML',
|
||||
priority: 'high',
|
||||
status: 'completed',
|
||||
createdAt: Date.now() - 10000000,
|
||||
},
|
||||
{
|
||||
id: 'idea-2',
|
||||
title: 'Real-time Collaboration',
|
||||
description: 'Allow multiple developers to work on the same project simultaneously',
|
||||
category: 'Collaboration',
|
||||
priority: 'high',
|
||||
status: 'idea',
|
||||
createdAt: Date.now() - 9000000,
|
||||
},
|
||||
{
|
||||
id: 'idea-3',
|
||||
title: 'Component Marketplace',
|
||||
description: 'A marketplace where users can share and download pre-built components',
|
||||
category: 'Community',
|
||||
priority: 'medium',
|
||||
status: 'idea',
|
||||
createdAt: Date.now() - 8000000,
|
||||
},
|
||||
]
|
||||
type SeedIdea = Omit<FeatureIdea, 'createdAt'> & { createdAtOffset: number }
|
||||
|
||||
const buildSeedIdeas = (): FeatureIdea[] => {
|
||||
const now = Date.now()
|
||||
return (seedData.ideas as SeedIdea[]).map((idea) => ({
|
||||
...idea,
|
||||
createdAt: now + idea.createdAtOffset,
|
||||
}))
|
||||
}
|
||||
|
||||
const SEED_IDEAS = buildSeedIdeas()
|
||||
|
||||
export function useFeatureIdeas() {
|
||||
const [ideas, setIdeas] = useKV<FeatureIdea[]>('feature-ideas', SEED_IDEAS)
|
||||
|
||||
Reference in New Issue
Block a user