feat: basic card component

This commit is contained in:
scarqin
2024-12-23 14:00:24 +08:00
parent 42cc086bd0
commit 6dbef3f743
3 changed files with 213 additions and 22 deletions
@@ -1,46 +1,147 @@
'use client'
import { Icon } from '@iconify/react'
import type { Edge, Node } from '@xyflow/react'
import { addEdge, Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState } from '@xyflow/react'
import { addEdge, Background, Controls, ReactFlow, useEdgesState, useNodesState } from '@xyflow/react'
import '@xyflow/react/dist/style.css'
import React, { useCallback } from 'react'
import './styles.css'
interface ModelCardProps {
title: string
status: 'success' | 'failure'
defaultModel: string
onSettingClick?: () => void
}
const ModelCard: React.FC<ModelCardProps> = ({ title, status, defaultModel, onSettingClick }) => (
<div className="bg-white rounded-lg shadow-sm p-4 w-[280px] border border-[#ededed]">
<div className="flex justify-between items-center mb-3">
<div className="flex gap-2 items-center">
<Icon icon="mdi:robot" className="text-xl text-[--primary-color]" />
<span className="font-medium text-gray-800">{title}</span>
</div>
<div className="flex gap-2 items-center">
<span
className={`w-2 h-2 rounded-full ${status === 'success' ? 'bg-green-500' : 'bg-red-500'}`}
title={status === 'success' ? 'Connected' : 'Disconnected'}
/>
<Icon
icon="mdi:cog"
className="text-xl text-gray-400 cursor-pointer hover:text-[--primary-color]"
onClick={onSettingClick}
/>
</div>
</div>
<div className="text-sm text-gray-500">{defaultModel}</div>
</div>
)
interface KeyStatusCardProps {
keys: Array<'success' | 'failure'>
title: string
}
const KeyStatusCard: React.FC<KeyStatusCardProps> = ({ keys, title }) => (
<div className="bg-white rounded-lg shadow-sm p-4 w-[280px] border border-[#ededed]">
<div className="flex items-center mb-3">
<span className="text-sm font-medium text-gray-700">{title}</span>
</div>
<div className="grid grid-cols-4 gap-2">
{keys.map((status, index) => (
<div
key={index}
className={`aspect-square rounded-md ${status === 'success' ? 'bg-green-500' : 'bg-red-500'}`}
title={status === 'success' ? 'Active' : 'Inactive'}
/>
))}
</div>
</div>
)
const ServiceCard: React.FC = () => (
<div className="bg-white rounded-lg shadow-sm p-4 w-[200px] border border-[#ededed]">
<div className="flex flex-col gap-3 items-center">
<Icon icon="mdi:robot" className="text-3xl text-[--primary-color]" />
<span className="font-medium text-gray-800">AI Service</span>
</div>
</div>
)
const initialNodes: Node[] = [
{
id: '1',
position: { x: 100, y: 100 },
data: { label: 'Node 1' },
type: 'input'
id: 'service',
type: 'custom',
position: { x: 50, y: 100 },
data: { component: <ServiceCard /> }
},
{
id: '2',
position: { x: 100, y: 200 },
data: { label: 'Node 2' }
id: 'openai',
type: 'custom',
position: { x: 400, y: 50 },
data: {
component: <ModelCard title="OpenAI" status="success" defaultModel="gpt-4" />
}
},
{
id: '3',
position: { x: 100, y: 300 },
data: { label: 'Node 3' },
type: 'output'
id: 'anthropic',
type: 'custom',
position: { x: 400, y: 200 },
data: {
component: <ModelCard title="Anthropic" status="success" defaultModel="claude-2" />
}
},
{
id: 'gemini',
type: 'custom',
position: { x: 400, y: 350 },
data: {
component: <ModelCard title="Google Gemini" status="failure" defaultModel="gemini-pro" />
}
},
{
id: 'openai-keys',
type: 'custom',
position: { x: 750, y: 50 },
data: {
component: <KeyStatusCard title="API Keys" keys={['success', 'success', 'failure', 'success']} />
}
},
{
id: 'anthropic-keys',
type: 'custom',
position: { x: 750, y: 200 },
data: {
component: <KeyStatusCard title="API Keys" keys={['success', 'success', 'success']} />
}
},
{
id: 'gemini-keys',
type: 'custom',
position: { x: 750, y: 350 },
data: {
component: <KeyStatusCard title="API Keys" keys={['failure', 'failure']} />
}
}
]
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e2-3', source: '2', target: '3' }
{ id: 'service-openai', source: 'service', target: 'openai', animated: true, label: 'Connected' },
{ id: 'service-anthropic', source: 'service', target: 'anthropic', animated: true, label: 'Connected' },
{ id: 'service-gemini', source: 'service', target: 'gemini', animated: true, label: 'Disconnected' },
{ id: 'openai-keys', source: 'openai', target: 'openai-keys', animated: true },
{ id: 'anthropic-keys', source: 'anthropic', target: 'anthropic-keys', animated: true },
{ id: 'gemini-keys', source: 'gemini', target: 'gemini-keys', animated: true }
]
const Playground: React.FC = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
const onConnect = useCallback(
(params: any) => {
setEdges((eds) => addEdge(params, eds))
},
[setEdges]
)
const onConnect = useCallback((params: any) => setEdges((eds) => addEdge(params, eds)), [setEdges])
return (
<div style={{ width: '100%', height: '600px' }}>
<div className="w-full h-screen bg-gray-50">
<ReactFlow
nodes={nodes}
edges={edges}
@@ -51,7 +152,6 @@ const Playground: React.FC = () => {
>
<Background />
<Controls />
<MiniMap />
</ReactFlow>
</div>
)
@@ -0,0 +1,91 @@
/* Flow Chart Styles */
.react-flow__node {
padding: 0;
border-radius: 8px;
min-width: 150px;
}
.react-flow__edge-path {
stroke: var(--primary-color);
stroke-width: 2;
}
.react-flow__edge-text {
font-size: 12px;
fill: #666;
}
.react-flow__controls {
box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
border: 1px solid #ededed;
}
.react-flow__controls-button {
border: none;
background: white;
border-bottom: 1px solid #ededed;
box-sizing: content-box;
display: flex;
justify-content: center;
align-items: center;
width: 16px;
height: 16px;
cursor: pointer;
user-select: none;
padding: 5px;
}
.react-flow__controls-button:hover {
background: #f8f8f8;
}
/* Custom Node Styles */
.custom-node {
background: white;
border: 1px solid #ededed;
border-radius: 8px;
padding: 10px;
width: 280px;
}
.custom-node:hover {
box-shadow: 0 0 0 1px var(--primary-color);
}
/* API Key Status Grid */
.api-key-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.api-key-status {
aspect-ratio: 1;
border-radius: 4px;
transition: all 0.2s ease;
}
.api-key-status:hover {
transform: scale(1.05);
}
/* Responsive Breakpoints */
@media (max-width: 768px) {
.custom-node {
width: 240px;
}
.api-key-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 576px) {
.custom-node {
width: 200px;
}
.api-key-grid {
grid-template-columns: repeat(2, 1fr);
}
}