Skip to content

Commit c60eff8

Browse files
committed
feat: add audio, image, text, and video node components with associated types
1 parent 7357ab7 commit c60eff8

File tree

8 files changed

+394
-27
lines changed

8 files changed

+394
-27
lines changed

frontend/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export default function App() {
4141
></div>
4242
<SidebarProvider>
4343
<AppSidebar />
44-
<SidebarInset className="m-2 rounded-2xl shadow z-10 border border-border/10">
44+
<SidebarInset className="m-2 rounded-2xl z-10 border">
4545
<ProjectList onProjectClick={handleProjectClick} />
4646
</SidebarInset>
4747
</SidebarProvider>

frontend/src/components/canvas-view.tsx

Lines changed: 79 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useCallback } from "react";
1+
import { useState, useCallback, useMemo } from "react";
22
import {
33
ReactFlow,
44
MiniMap,
@@ -16,33 +16,19 @@ import "@xyflow/react/dist/style.css";
1616
import { Button } from "@/components/ui/button";
1717
import { Input } from "@/components/ui/input";
1818
import { Textarea } from "@/components/ui/textarea";
19-
import { ArrowLeft, MessageSquare, Plus, Square, Circle, X } from "lucide-react";
19+
import { ArrowLeft, MessageSquare, Plus, FileText, Image, Video, Music, X } from "lucide-react";
2020
import { Card } from "@/components/ui/card";
21+
import { TextNode, ImageNode, VideoNode, AudioNode, type NodeType } from "./nodes";
2122

2223
interface CanvasViewProps {
2324
projectId: string;
2425
projectName: string;
2526
onBack: () => void;
2627
}
2728

28-
const initialNodes: Node[] = [
29-
{
30-
id: "1",
31-
type: "default",
32-
data: { label: "开始节点" },
33-
position: { x: 250, y: 100 },
34-
},
35-
{
36-
id: "2",
37-
type: "default",
38-
data: { label: "处理节点" },
39-
position: { x: 250, y: 250 },
40-
},
41-
];
29+
const initialNodes: Node[] = [];
4230

43-
const initialEdges: Edge[] = [
44-
{ id: "e1-2", source: "1", target: "2", animated: true },
45-
];
31+
const initialEdges: Edge[] = [];
4632

4733
export function CanvasView({
4834
projectId,
@@ -54,12 +40,55 @@ export function CanvasView({
5440
const [chatMessage, setChatMessage] = useState("");
5541
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
5642
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
43+
const [nodeIdCounter, setNodeIdCounter] = useState(1);
44+
45+
// Define custom node types
46+
const nodeTypes = useMemo(
47+
() => ({
48+
text: TextNode,
49+
image: ImageNode,
50+
video: VideoNode,
51+
audio: AudioNode,
52+
}),
53+
[]
54+
);
5755

5856
const onConnect = useCallback(
59-
(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
57+
(connection: Connection) => {
58+
// Create animated edge with data flow
59+
const newEdge = {
60+
...connection,
61+
animated: true,
62+
style: { stroke: '#3b82f6' },
63+
};
64+
setEdges((eds) => addEdge(newEdge, eds));
65+
66+
// TODO: Trigger data processing from source to target node
67+
// This would involve calling your AI model API
68+
},
6069
[setEdges]
6170
);
6271

72+
const addNode = useCallback(
73+
(type: NodeType) => {
74+
const newNode: Node = {
75+
id: `node-${nodeIdCounter}`,
76+
type: type,
77+
position: {
78+
x: Math.random() * 400 + 100,
79+
y: Math.random() * 400 + 100
80+
},
81+
data: {
82+
label: `${type === 'text' ? '文本' : type === 'image' ? '图片' : type === 'video' ? '视频' : '音频'}节点 ${nodeIdCounter}`,
83+
type: type,
84+
},
85+
};
86+
setNodes((nds) => [...nds, newNode]);
87+
setNodeIdCounter((c) => c + 1);
88+
},
89+
[nodeIdCounter, setNodes]
90+
);
91+
6392
return (
6493
<div className="flex h-full relative">
6594
{/* 主要内容区域 - 全屏 */}
@@ -93,14 +122,37 @@ export function CanvasView({
93122
<div className="absolute left-4 top-20 z-10 flex flex-col gap-2">
94123
<Card className="p-2 shadow-lg">
95124
<div className="flex flex-col gap-2">
96-
<Button variant="ghost" size="icon" title="添加节点">
97-
<Plus className="h-4 w-4" />
125+
<Button
126+
variant="ghost"
127+
size="icon"
128+
title="文本节点"
129+
onClick={() => addNode("text")}
130+
>
131+
<FileText className="h-4 w-4" />
98132
</Button>
99-
<Button variant="ghost" size="icon" title="矩形">
100-
<Square className="h-4 w-4" />
133+
<Button
134+
variant="ghost"
135+
size="icon"
136+
title="图片节点"
137+
onClick={() => addNode("image")}
138+
>
139+
<Image className="h-4 w-4" />
140+
</Button>
141+
<Button
142+
variant="ghost"
143+
size="icon"
144+
title="视频节点"
145+
onClick={() => addNode("video")}
146+
>
147+
<Video className="h-4 w-4" />
101148
</Button>
102-
<Button variant="ghost" size="icon" title="圆形">
103-
<Circle className="h-4 w-4" />
149+
<Button
150+
variant="ghost"
151+
size="icon"
152+
title="音频节点"
153+
onClick={() => addNode("audio")}
154+
>
155+
<Music className="h-4 w-4" />
104156
</Button>
105157
</div>
106158
</Card>
@@ -114,6 +166,7 @@ export function CanvasView({
114166
onNodesChange={onNodesChange}
115167
onEdgesChange={onEdgesChange}
116168
onConnect={onConnect}
169+
nodeTypes={nodeTypes}
117170
fitView
118171
>
119172
<Controls />
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { memo } from "react";
2+
import { Handle, Position, type NodeProps } from "@xyflow/react";
3+
import { Card } from "@/components/ui/card";
4+
import { Textarea } from "@/components/ui/textarea";
5+
import { Music, Loader2 } from "lucide-react";
6+
import type { AudioNodeData } from "./types";
7+
8+
export const AudioNode = memo(({ data, selected }: NodeProps) => {
9+
const nodeData = data as unknown as AudioNodeData;
10+
11+
return (
12+
<Card
13+
className={`max-w-75 py-0! gap-0 ${
14+
selected ? "ring-2 ring-primary" : ""
15+
} ${nodeData.processing ? "opacity-70" : ""}`}
16+
>
17+
<Handle type="target" position={Position.Left} />
18+
19+
<div className="p-3 border-b bg-muted/50 flex items-center gap-2">
20+
<Music className="h-4 w-4 text-green-500" />
21+
<span className="font-semibold text-sm">{nodeData.label}</span>
22+
{nodeData.processing && <Loader2 className="h-3 w-3 animate-spin ml-auto" />}
23+
</div>
24+
25+
<div className="p-3 space-y-2">
26+
<div>
27+
<label className="text-xs text-muted-foreground block mb-1">提示词</label>
28+
<Textarea
29+
placeholder="输入音频处理提示词..."
30+
className="min-h-15 text-sm"
31+
defaultValue={nodeData.prompt}
32+
/>
33+
</div>
34+
35+
{nodeData.audioUrl && (
36+
<div>
37+
<label className="text-xs text-muted-foreground block mb-1">音频</label>
38+
<div className="w-full p-2 bg-muted rounded-md">
39+
<audio
40+
src={nodeData.audioUrl}
41+
controls
42+
className="w-full"
43+
/>
44+
</div>
45+
</div>
46+
)}
47+
48+
{nodeData.input && !nodeData.audioUrl && (
49+
<div>
50+
<label className="text-xs text-muted-foreground block mb-1">输入数据</label>
51+
<div className="text-xs p-2 bg-muted rounded-md max-h-25 overflow-auto">
52+
{typeof nodeData.input === 'string' ? nodeData.input : JSON.stringify(nodeData.input)}
53+
</div>
54+
</div>
55+
)}
56+
57+
{nodeData.error && (
58+
<div className="text-xs text-destructive p-2 bg-destructive/10 rounded-md">
59+
{nodeData.error}
60+
</div>
61+
)}
62+
</div>
63+
64+
<Handle type="source" position={Position.Right} />
65+
</Card>
66+
);
67+
});
68+
69+
AudioNode.displayName = "AudioNode";
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { memo } from "react";
2+
import { Handle, Position, type NodeProps } from "@xyflow/react";
3+
import { Card } from "@/components/ui/card";
4+
import { Textarea } from "@/components/ui/textarea";
5+
import { Image as ImageIcon, Loader2 } from "lucide-react";
6+
import type { ImageNodeData } from "./types";
7+
8+
export const ImageNode = memo(({ data, selected }: NodeProps) => {
9+
const nodeData = data as unknown as ImageNodeData;
10+
11+
return (
12+
<Card
13+
className={`max-w-75 py-0! gap-0 ${
14+
selected ? "ring-2 ring-primary" : ""
15+
} ${nodeData.processing ? "opacity-70" : ""}`}
16+
>
17+
<Handle type="target" position={Position.Left} />
18+
19+
<div className="p-3 border-b bg-muted/50 flex items-center gap-2">
20+
<ImageIcon className="h-4 w-4 text-blue-500" />
21+
<span className="font-semibold text-sm">{nodeData.label}</span>
22+
{nodeData.processing && <Loader2 className="h-3 w-3 animate-spin ml-auto" />}
23+
</div>
24+
25+
<div className="p-3 space-y-2">
26+
<div>
27+
<label className="text-xs text-muted-foreground block mb-1">提示词</label>
28+
<Textarea
29+
placeholder="输入图片处理提示词..."
30+
className="min-h-15 text-sm"
31+
defaultValue={nodeData.prompt}
32+
/>
33+
</div>
34+
35+
{nodeData.imageUrl && (
36+
<div>
37+
<label className="text-xs text-muted-foreground block mb-1">图片</label>
38+
<div className="relative w-full h-40 bg-muted rounded-md overflow-hidden">
39+
<img
40+
src={nodeData.imageUrl}
41+
alt="Preview"
42+
className="w-full h-full object-contain"
43+
/>
44+
</div>
45+
</div>
46+
)}
47+
48+
{nodeData.input && !nodeData.imageUrl && (
49+
<div>
50+
<label className="text-xs text-muted-foreground block mb-1">输入数据</label>
51+
<div className="text-xs p-2 bg-muted rounded-md max-h-25 overflow-auto">
52+
{typeof nodeData.input === 'string' ? nodeData.input : JSON.stringify(nodeData.input)}
53+
</div>
54+
</div>
55+
)}
56+
57+
{nodeData.error && (
58+
<div className="text-xs text-destructive p-2 bg-destructive/10 rounded-md">
59+
{nodeData.error}
60+
</div>
61+
)}
62+
</div>
63+
64+
<Handle type="source" position={Position.Right} />
65+
</Card>
66+
);
67+
});
68+
69+
ImageNode.displayName = "ImageNode";
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { memo } from "react";
2+
import { Handle, Position, type NodeProps } from "@xyflow/react";
3+
import { Card } from "@/components/ui/card";
4+
import { Textarea } from "@/components/ui/textarea";
5+
import { FileText, Loader2 } from "lucide-react";
6+
import type { TextNodeData } from "./types";
7+
8+
export const TextNode = memo(({ data, selected }: NodeProps) => {
9+
const nodeData = data as unknown as TextNodeData;
10+
11+
return (
12+
<Card
13+
className={`max-w-75 py-0! gap-0 ${
14+
selected ? "ring-2 ring-primary" : ""
15+
} ${nodeData.processing ? "opacity-70" : ""}`}
16+
>
17+
<Handle type="target" position={Position.Left} />
18+
19+
<div className="p-3 border-b bg-muted/50 flex items-center gap-2">
20+
<FileText className="h-4 w-4 text-primary" />
21+
<span className="font-semibold text-sm">{nodeData.label}</span>
22+
{nodeData.processing && <Loader2 className="h-3 w-3 animate-spin ml-auto" />}
23+
</div>
24+
25+
<div className="p-3 space-y-2">
26+
<div>
27+
<label className="text-xs text-muted-foreground block mb-1">提示词</label>
28+
<Textarea
29+
placeholder="输入 AI 处理提示词..."
30+
className="min-h-15 text-sm"
31+
defaultValue={nodeData.prompt}
32+
/>
33+
</div>
34+
35+
{nodeData.input && (
36+
<div>
37+
<label className="text-xs text-muted-foreground block mb-1">输入</label>
38+
<div className="text-xs p-2 bg-muted rounded-md max-h-25 overflow-auto">
39+
{String(nodeData.input)}
40+
</div>
41+
</div>
42+
)}
43+
44+
{nodeData.output && (
45+
<div>
46+
<label className="text-xs text-muted-foreground block mb-1">输出</label>
47+
<div className="text-xs p-2 bg-muted rounded-md max-h-25 overflow-auto">
48+
{String(nodeData.output)}
49+
</div>
50+
</div>
51+
)}
52+
53+
{nodeData.error && (
54+
<div className="text-xs text-destructive p-2 bg-destructive/10 rounded-md">
55+
{nodeData.error}
56+
</div>
57+
)}
58+
</div>
59+
60+
<Handle type="source" position={Position.Right} />
61+
</Card>
62+
);
63+
});
64+
65+
TextNode.displayName = "TextNode";

0 commit comments

Comments
 (0)