+
{children}
@@ -87,6 +89,9 @@ const App: React.FC = () => {
} />
} />
} />
+
} />
+
} />
+
} />
} />
@@ -94,4 +99,4 @@ const App: React.FC = () => {
);
};
-export default App;
\ No newline at end of file
+export default App;
diff --git a/pages/Assistants.tsx b/pages/Assistants.tsx
index 52a90d5..a4417db 100644
--- a/pages/Assistants.tsx
+++ b/pages/Assistants.tsx
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from 'react';
-import { Plus, Search, Play, Copy, Trash2, Edit2, Mic, MessageSquare, Save, Video, PhoneOff, Camera, ArrowLeftRight, Send, Phone, MoreHorizontal, Rocket, AlertTriangle } from 'lucide-react';
+import { Plus, Search, Play, Copy, Trash2, Edit2, Mic, MessageSquare, Save, Video, PhoneOff, Camera, ArrowLeftRight, Send, Phone, MoreHorizontal, Rocket, AlertTriangle, PhoneCall } from 'lucide-react';
import { Button, Input, Card, Badge, Drawer, Dialog } from '../components/UI';
import { mockAssistants, mockKnowledgeBases } from '../services/mockData';
import { Assistant, TabValue } from '../types';
@@ -385,12 +385,12 @@ const BotIcon = ({className}: {className?: string}) => (
);
// --- Debug Drawer Component ---
-
-const DebugDrawer: React.FC<{ isOpen: boolean; onClose: () => void; assistant: Assistant }> = ({ isOpen, onClose, assistant }) => {
+export const DebugDrawer: React.FC<{ isOpen: boolean; onClose: () => void; assistant: Assistant }> = ({ isOpen, onClose, assistant }) => {
const [mode, setMode] = useState<'text' | 'voice' | 'video'>('text');
const [messages, setMessages] = useState<{role: 'user' | 'model', text: string}[]>([]);
const [inputText, setInputText] = useState('');
const [isLoading, setIsLoading] = useState(false);
+ const [callStatus, setCallStatus] = useState<'idle' | 'calling' | 'active'>('idle');
// Media State
const videoRef = useRef
(null);
@@ -400,19 +400,24 @@ const DebugDrawer: React.FC<{ isOpen: boolean; onClose: () => void; assistant: A
const [devices, setDevices] = useState([]);
const [selectedCamera, setSelectedCamera] = useState('');
const [selectedMic, setSelectedMic] = useState('');
- const [isSwapped, setIsSwapped] = useState(false); // False: AI is Big, Local is Small. True: Local is Big, AI is Small.
+ const [isSwapped, setIsSwapped] = useState(false);
- // Initialize with opener
+ // Initialize
useEffect(() => {
if (isOpen) {
- setMessages([{ role: 'model', text: assistant.opener || "Hello!" }]);
+ if (mode === 'text') {
+ setMessages([{ role: 'model', text: assistant.opener || "Hello!" }]);
+ } else {
+ setMessages([]);
+ setCallStatus('idle');
+ }
} else {
- // Reset and stop media when closed
setMode('text');
stopMedia();
setIsSwapped(false);
+ setCallStatus('idle');
}
- }, [isOpen, assistant]);
+ }, [isOpen, assistant, mode]);
// Auto-scroll logic
useEffect(() => {
@@ -429,13 +434,10 @@ const DebugDrawer: React.FC<{ isOpen: boolean; onClose: () => void; assistant: A
await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
const dev = await navigator.mediaDevices.enumerateDevices();
setDevices(dev);
-
const cams = dev.filter(d => d.kind === 'videoinput');
const mics = dev.filter(d => d.kind === 'audioinput');
-
if (cams.length > 0 && !selectedCamera) setSelectedCamera(cams[0].deviceId);
if (mics.length > 0 && !selectedMic) setSelectedMic(mics[0].deviceId);
-
} catch (e) {
console.error("Error enumerating devices", e);
}
@@ -444,7 +446,6 @@ const DebugDrawer: React.FC<{ isOpen: boolean; onClose: () => void; assistant: A
}
}, [isOpen, mode]);
- // Handle Video/Media stream
const stopMedia = () => {
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
@@ -454,7 +455,7 @@ const DebugDrawer: React.FC<{ isOpen: boolean; onClose: () => void; assistant: A
useEffect(() => {
const handleStream = async () => {
- if (isOpen && mode === 'video') {
+ if (isOpen && mode === 'video' && callStatus === 'active') {
try {
stopMedia();
const constraints = {
@@ -469,14 +470,28 @@ const DebugDrawer: React.FC<{ isOpen: boolean; onClose: () => void; assistant: A
} catch (err) {
console.error("Failed to access camera/mic:", err);
}
- } else {
+ } else if (callStatus !== 'active') {
stopMedia();
}
};
handleStream();
return () => stopMedia();
- }, [mode, isOpen, selectedCamera, selectedMic]);
+ }, [mode, isOpen, selectedCamera, selectedMic, callStatus]);
+
+ const handleCall = () => {
+ setCallStatus('calling');
+ setTimeout(() => {
+ setCallStatus('active');
+ setMessages([{ role: 'model', text: assistant.opener || "Hello!" }]);
+ }, 1500);
+ };
+
+ const handleHangup = () => {
+ stopMedia();
+ setCallStatus('idle');
+ setMessages([]);
+ };
const handleSend = async () => {
if (!inputText.trim()) return;
@@ -509,12 +524,6 @@ const DebugDrawer: React.FC<{ isOpen: boolean; onClose: () => void; assistant: A
}
};
- const handleClose = () => {
- stopMedia();
- onClose();
- };
-
- // Reusable Messages List Component
const TranscriptionLog = () => (
{messages.length === 0 &&
暂无转写记录
}
@@ -530,171 +539,113 @@ const DebugDrawer: React.FC<{ isOpen: boolean; onClose: () => void; assistant: A
);
- // Helper to render the Local Video Element
const renderLocalVideo = (isSmall: boolean) => (
);
- // Helper to render the "Remote" AI Video Element (Simulated)
const renderRemoteVideo = (isSmall: boolean) => (
- {!isSmall && (
-
- {assistant.name}
-
- )}
+ {!isSmall &&
{assistant.name}
}
);
return (
-
+ { handleHangup(); onClose(); }} title={`调试: ${assistant.name}`}>
- {/* Mode Toggle */}
-
-
-
+ {(['text', 'voice', 'video'] as const).map(m => (
+
+ ))}
- {/* Content Area */}
-
- {mode === 'text' &&
}
-
- {mode === 'voice' && (
-
- {/* Visualizer Area */}
-
- {/* Transcript */}
-
实时转写 / Live Transcription
-
-
- )}
-
- {mode === 'video' && (
-
- {/* Video Area (Top) */}
-
- {/* Device Settings Bar */}
-
-
-
-
-
-
-
-
-
- {/* Video Container (PiP) */}
-
- {/* Main Window */}
-
- {isSwapped ? renderLocalVideo(false) : renderRemoteVideo(false)}
-
- {/* Small Window */}
-
- {isSwapped ? renderRemoteVideo(true) : renderLocalVideo(true)}
-
- {/* Swap Button */}
-
-
-
+
+ {mode === 'text' ? (
+
+ ) : callStatus === 'idle' ? (
+
+
+
+
+ {mode === 'voice' ? : }
-
- {/* Transcript Area (Bottom) */}
-
-
实时转写 / Live Transcription
-
+
-
- )}
-
-
- {/* Footer Actions - Unified Input */}
-
- {(mode === 'voice' || mode === 'video') && (
-
- )}
-
-
+ ) : callStatus === 'calling' ? (
+
+
+
+
CALLING...
+
正在连接 AI 服务
+
+
+
+ ) : (
+
+ {mode === 'voice' ? (
+
+ ) : (
+
+
+
+
+
+
+
+
{isSwapped ? renderLocalVideo(false) : renderRemoteVideo(false)}
+
{isSwapped ? renderRemoteVideo(true) : renderLocalVideo(true)}
+
+
+
+
+
+ )}
+
+
+ )}
+
+
+
diff --git a/pages/Dashboard.tsx b/pages/Dashboard.tsx
index 33306bd..3de0a9e 100644
--- a/pages/Dashboard.tsx
+++ b/pages/Dashboard.tsx
@@ -1,5 +1,6 @@
+
import React, { useState, useMemo } from 'react';
-import { Phone, CheckCircle, Clock, UserCheck, Activity, Filter } from 'lucide-react';
+import { Phone, CheckCircle, Clock, UserCheck, Activity, Filter, ChevronDown, BarChart3, HelpCircle, Mail } from 'lucide-react';
import { Card, Button } from '../components/UI';
import { mockAssistants, getDashboardStats } from '../services/mockData';
@@ -12,84 +13,133 @@ export const DashboardPage: React.FC = () => {
}, [timeRange, selectedAssistantId]);
return (
-
-
-
首页概览
+
+
- {/* Filters */}
-
-
-
-
+ {/* 1. Utility Row (Top Navigation Actions) */}
+
+
+
+
+
+ {/* 2. Welcome Row */}
+
+
+ 欢迎, Admin User
+
+
+ 系统状态:
+
+
+ HEALTHY
+
+
+
+
+ {/* 3. Section Header: Title + Filters aligned perfectly */}
+
+
+
+
+
+
+
用量标准
+ Metrics Overview
+
-
-
- {(['week', 'month', 'year'] as const).map((r) => (
-
- ))}
+
+ {/* Filters Group (Aligned Right) */}
+
+ {/* Assistant Selector */}
+
+
+
+
+
+
+
+
+
+
+ {/* Time Range Selector */}
+
+ {(['week', 'month', 'year'] as const).map((r) => (
+
+ ))}
+
-
- {/* Metrics Grid */}
-
- }
- trend="+12.5% 较上期"
- />
- }
- trend="+2.1% 较上期"
- />
- }
- trend="-0.5% 较上期"
- />
- }
- trend="+5% 较上期"
- />
-
+ {/* 4. Metrics Grid (Cards) */}
+
+ }
+ trend="+12.5% UP"
+ />
+ }
+ trend="+2.1% UP"
+ />
+ }
+ trend="-0.5% LOW"
+ />
+ }
+ trend="+5% STABLE"
+ />
+
- {/* Charts Section */}
-
-
-
-
-
-
- 通话趋势图
-
-
展示选定时间范围内的通话量变化
-
-
-
-
-
-
+ {/* 5. Charts Section */}
+
+
+
+
+
+
+ 通话趋势 (Performance Insight)
+
+
REAL-TIME DATA PROCESSING PIPELINE ENABLED
+
+
+
+
+
+
+
+
);
@@ -98,14 +148,20 @@ export const DashboardPage: React.FC = () => {
// --- Sub Components ---
const StatCard: React.FC<{ title: string; value: string; icon: React.ReactNode; trend?: string }> = ({ title, value, icon, trend }) => (
-
-
-
{title}
- {icon}
+
+
-
-
{value}
- {trend &&
{trend}
}
+
+
{value}
+ {trend && (
+
+ {trend}
+
+ )}
);
@@ -131,9 +187,16 @@ const SimpleAreaChart: React.FC<{ data: { label: string, value: number }[] }> =
return (
{/* X-Axis Labels */}
-
- {data.filter((_, i) => i % Math.ceil(data.length / 6) === 0).map((d, i) => (
+
+ {data.filter((_, i) => i % Math.ceil(data.length / 7) === 0).map((d, i) => (
{d.label}
))}
diff --git a/pages/WorkflowEditor.tsx b/pages/WorkflowEditor.tsx
new file mode 100644
index 0000000..b3990a5
--- /dev/null
+++ b/pages/WorkflowEditor.tsx
@@ -0,0 +1,402 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
+import { ArrowLeft, Play, Save, Rocket, Plus, Bot, UserCheck, Wrench, Ban, Zap, X, Copy, MousePointer2 } from 'lucide-react';
+import { Button, Input, Badge } from '../components/UI';
+import { mockAssistants, mockKnowledgeBases, mockWorkflows } from '../services/mockData';
+import { WorkflowNode, WorkflowEdge, Workflow } from '../types';
+import { DebugDrawer } from './Assistants';
+
+export const WorkflowEditorPage: React.FC = () => {
+ const navigate = useNavigate();
+ const { id } = useParams();
+ const [searchParams] = useSearchParams();
+
+ // Template data for new workflows
+ const templateName = searchParams.get('name');
+ const templateType = searchParams.get('template');
+
+ // Find initial workflow or create a new one
+ const existingWf = mockWorkflows.find(w => w.id === id);
+ const [name, setName] = useState(templateName || existingWf?.name || '新工作流');
+ const [nodes, setNodes] = useState
(() => {
+ if (existingWf) return existingWf.nodes;
+ if (templateType === 'lead') {
+ return [
+ {
+ name: 'introduction',
+ type: 'conversation',
+ isStart: true,
+ metadata: { position: { x: 100, y: 100 } },
+ prompt: "You are Morgan from GrowthPartners. Start with: 'Hello, this is Morgan from GrowthPartners. We help businesses improve their operational efficiency through custom software solutions. Do you have a few minutes to chat about how we might be able to help your business?'",
+ messagePlan: { firstMessage: "Hello, this is Morgan from GrowthPartners. Do you have a few minutes to chat?" }
+ },
+ {
+ name: 'need_discovery',
+ type: 'conversation',
+ metadata: { position: { x: 450, y: 250 } },
+ prompt: "Conduct need discovery by asking about business challenges...",
+ variableExtractionPlan: {
+ output: [
+ { title: 'industry', type: 'string', description: 'user industry' },
+ { title: 'pain_points', type: 'string', description: 'main challenges' }
+ ]
+ }
+ },
+ {
+ name: 'hangup_node',
+ type: 'end',
+ metadata: { position: { x: 450, y: 550 } },
+ tool: {
+ type: 'endCall',
+ function: { name: 'hangup', parameters: {} },
+ messages: [{ type: 'request-start', content: 'Thank you for your time!', blocking: true }]
+ }
+ }
+ ];
+ }
+ return [
+ {
+ name: 'start_node',
+ type: 'conversation',
+ isStart: true,
+ metadata: { position: { x: 200, y: 200 } },
+ prompt: '欢迎对话系统...',
+ messagePlan: { firstMessage: '你好!' }
+ }
+ ];
+ });
+ const [edges, setEdges] = useState(existingWf?.edges || []);
+
+ const [selectedNodeName, setSelectedNodeName] = useState(null);
+ const [isAddMenuOpen, setIsAddMenuOpen] = useState(false);
+ const [isDebugOpen, setIsDebugOpen] = useState(false);
+ const [zoom, setZoom] = useState(1);
+ const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
+
+ const [draggingNodeName, setDraggingNodeName] = useState(null);
+ const [isPanning, setIsPanning] = useState(false);
+ const dragOffset = useRef({ x: 0, y: 0 });
+ const panStart = useRef({ x: 0, y: 0 });
+
+ const selectedNode = nodes.find(n => n.name === selectedNodeName);
+
+ // Scroll Zoom handler
+ const handleWheel = (e: React.WheelEvent) => {
+ e.preventDefault();
+ const zoomSpeed = 0.001;
+ const newZoom = Math.min(Math.max(zoom - e.deltaY * zoomSpeed, 0.2), 3);
+ setZoom(newZoom);
+ };
+
+ // Middle mouse click handler for panning
+ const handleMouseDown = (e: React.MouseEvent) => {
+ // Button 1 is middle mouse
+ if (e.button === 1) {
+ e.preventDefault();
+ setIsPanning(true);
+ panStart.current = { x: e.clientX - panOffset.x, y: e.clientY - panOffset.y };
+ }
+ };
+
+ const handleNodeMouseDown = (e: React.MouseEvent, nodeName: string) => {
+ if (e.button !== 0) return; // Only left click for nodes
+ e.stopPropagation();
+ setSelectedNodeName(nodeName);
+ setDraggingNodeName(nodeName);
+ const node = nodes.find(n => n.name === nodeName);
+ if (node) {
+ dragOffset.current = {
+ x: e.clientX - node.metadata.position.x * zoom,
+ y: e.clientY - node.metadata.position.y * zoom
+ };
+ }
+ };
+
+ useEffect(() => {
+ const handleMouseMove = (e: MouseEvent) => {
+ if (draggingNodeName) {
+ setNodes(prev => prev.map(n =>
+ n.name === draggingNodeName
+ ? { ...n, metadata: { ...n.metadata, position: { x: (e.clientX - dragOffset.current.x) / zoom, y: (e.clientY - dragOffset.current.y) / zoom } } }
+ : n
+ ));
+ } else if (isPanning) {
+ setPanOffset({
+ x: e.clientX - panStart.current.x,
+ y: e.clientY - panStart.current.y
+ });
+ }
+ };
+ const handleMouseUp = (e: MouseEvent) => {
+ setDraggingNodeName(null);
+ setIsPanning(false);
+ };
+
+ window.addEventListener('mousemove', handleMouseMove);
+ window.addEventListener('mouseup', handleMouseUp);
+ return () => {
+ window.removeEventListener('mousemove', handleMouseMove);
+ window.removeEventListener('mouseup', handleMouseUp);
+ };
+ }, [draggingNodeName, isPanning, zoom]);
+
+ const addNode = (type: WorkflowNode['type']) => {
+ const newNode: WorkflowNode = {
+ name: `${type}_${Date.now()}`,
+ type,
+ metadata: { position: { x: (300 - panOffset.x) / zoom, y: (300 - panOffset.y) / zoom } },
+ prompt: type === 'conversation' ? '输入该节点的 Prompt...' : '',
+ tool: type === 'end' ? { type: 'endCall', function: { name: 'hangup', parameters: {} } } : undefined
+ };
+ setNodes([...nodes, newNode]);
+ setIsAddMenuOpen(false);
+ };
+
+ const updateNodeData = (field: string, value: any) => {
+ if (!selectedNodeName) return;
+ setNodes(prev => prev.map(n => n.name === selectedNodeName ? { ...n, [field]: value } : n));
+ };
+
+ const handleSave = () => {
+ const now = new Date().toISOString().replace('T', ' ').substring(0, 16);
+ const updatedWorkflow: Workflow = {
+ id: id || `wf_${Date.now()}`,
+ name,
+ nodeCount: nodes.length,
+ createdAt: existingWf?.createdAt || now,
+ updatedAt: now,
+ nodes,
+ edges,
+ };
+
+ if (id) {
+ const idx = mockWorkflows.findIndex(w => w.id === id);
+ if (idx !== -1) mockWorkflows[idx] = updatedWorkflow;
+ } else {
+ mockWorkflows.push(updatedWorkflow);
+ }
+ alert('保存成功!工作流已同步至列表。');
+ navigate('/workflows');
+ };
+
+ return (
+
+ {/* Editor Header */}
+
+
+ {/* Canvas Area */}
+
+ {/* Floating Controls */}
+
+
+ {isAddMenuOpen && (
+
+
+
+
+
+
+ )}
+
+ {Math.round(zoom * 100)}%
+ |
+ POS: {Math.round(panOffset.x)}, {Math.round(panOffset.y)}
+
+
+
+ {/* Scalable Container */}
+
+ {nodes.map(node => (
+
handleNodeMouseDown(e, node.name)}
+ style={{ left: node.metadata.position.x, top: node.metadata.position.y }}
+ className={`absolute w-56 p-4 rounded-xl border bg-card/70 backdrop-blur-sm cursor-grab active:cursor-grabbing group transition-shadow ${selectedNodeName === node.name ? 'border-primary shadow-[0_0_30px_rgba(6,182,212,0.3)]' : 'border-white/10 hover:border-white/30'}`}
+ >
+
+
+
+ {node.name}
+
+ {node.isStart &&
START}
+
+
+ {node.prompt || (node.tool ? `Action: ${node.tool.type}` : '待配置逻辑...')}
+
+
+ {/* Port simulation */}
+
+
+
+ ))}
+
+
+ {/* Bottom Right: Minimap */}
+
+
Minimap
+
+
+
+ {nodes.map((n, i) => {
+ // Simple scaling for minimap
+ const mx = (n.metadata.position.x / 10) % 100;
+ const my = (n.metadata.position.y / 10) % 100;
+ return (
+
+ );
+ })}
+
+
+ {/* Viewport visualization */}
+
+
+
+
+ {/* Navigator Guide */}
+
+ 鼠标中键拖拽画布 | 滚轮缩放
+
+
+
+ {/* Right Settings Panel */}
+ {selectedNode && (
+
+
+
节点设置
+
+
+
+
+
+ updateNodeData('name', e.target.value)}
+ className="h-8 text-xs font-mono text-white bg-white/10"
+ />
+
+
+
+
+ {selectedNode.type.toUpperCase()}
+
+
+ {selectedNode.type === 'conversation' && (
+ <>
+
+
+
+
+
+ updateNodeData('messagePlan', { ...selectedNode.messagePlan, firstMessage: e.target.value })}
+ className="h-8 text-xs text-white"
+ placeholder="进入该节点时 AI 主动发起的消息..."
+ />
+
+ >
+ )}
+
+ {selectedNode.type === 'end' && (
+
+
结束对话节点
+
到达此节点后,系统将根据配置执行挂断操作。
+
+ )}
+
+
+
+
+
+
+ )}
+
+ {/* Debug Side Drawer */}
+
setIsDebugOpen(false)}
+ assistant={mockAssistants[0]}
+ />
+
+ );
+};
+
+const NodeIcon = ({ type }: { type: WorkflowNode['type'] }) => {
+ switch (type) {
+ case 'conversation': return ;
+ case 'human': return ;
+ case 'tool': return ;
+ case 'end': return ;
+ default: return ;
+ }
+};
diff --git a/pages/Workflows.tsx b/pages/Workflows.tsx
new file mode 100644
index 0000000..0eec104
--- /dev/null
+++ b/pages/Workflows.tsx
@@ -0,0 +1,235 @@
+
+import React, { useState, useRef } from 'react';
+import { Search, Plus, Upload, MoreHorizontal, Code, Edit2, Copy, Trash2, Calendar, CloudUpload, File as FileIcon, X, Layout, FilePlus } from 'lucide-react';
+import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Dialog, Card } from '../components/UI';
+import { mockWorkflows } from '../services/mockData';
+import { useNavigate } from 'react-router-dom';
+
+export const WorkflowsPage: React.FC = () => {
+ const navigate = useNavigate();
+ const [workflows, setWorkflows] = useState(mockWorkflows);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [isUploadOpen, setIsUploadOpen] = useState(false);
+ const [isCreateOpen, setIsCreateOpen] = useState(false);
+ const [activeMenu, setActiveMenu] = useState(null);
+
+ // New Workflow State
+ const [newWfName, setNewWfName] = useState('');
+ const [selectedTemplate, setSelectedTemplate] = useState<'blank' | 'lead'>('blank');
+
+ const filteredWorkflows = workflows.filter(wf =>
+ wf.name.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+
+ const handleCreateWorkflow = () => {
+ if (!newWfName.trim()) {
+ alert('请输入工作流名称');
+ return;
+ }
+ setIsCreateOpen(false);
+ // Navigate to the editor with the template name and type as query params
+ navigate(`/workflows/new?name=${encodeURIComponent(newWfName)}&template=${selectedTemplate}`);
+ };
+
+ const handleDeleteWorkflow = (id: string) => {
+ if (confirm('确定要删除这个工作流吗?')) {
+ setWorkflows(prev => prev.filter(w => w.id !== id));
+ setActiveMenu(null);
+ }
+ };
+
+ return (
+
+
+
工作流
+
+
+
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ 名称
+ 节点数量
+ 创建时间
+ 更新时间
+ 操作
+
+
+
+ {filteredWorkflows.map(wf => (
+
+
+
+
+ {wf.nodeCount} 个节点
+ {wf.createdAt}
+ {wf.updatedAt}
+
+
+
+ {activeMenu === wf.id && (
+
+
+
+
+
+
+
+ )}
+
+
+ ))}
+ {filteredWorkflows.length === 0 && (
+
+ 暂无工作流数据
+
+ )}
+
+
+
+
+
setIsUploadOpen(false)} />
+
+ {/* Create Workflow Modal */}
+
+
+ );
+};
+
+const UploadJsonModal: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpen, onClose }) => {
+ const [dragActive, setDragActive] = useState(false);
+ const [file, setFile] = useState(null);
+ const inputRef = useRef(null);
+
+ const handleDrag = (e: React.DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setDragActive(e.type === "dragenter" || e.type === "dragover");
+ };
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault(); e.stopPropagation();
+ setDragActive(false);
+ if (e.dataTransfer.files?.[0]) setFile(e.dataTransfer.files[0]);
+ };
+
+ return (
+
+ );
+};
diff --git a/services/mockData.ts b/services/mockData.ts
index fcbb5fb..8c2ce1c 100644
--- a/services/mockData.ts
+++ b/services/mockData.ts
@@ -1,5 +1,5 @@
-import { Assistant, CallLog, KnowledgeBase, Voice } from '../types';
+import { Assistant, CallLog, KnowledgeBase, Voice, Workflow } from '../types';
export const mockAssistants: Assistant[] = [
{
@@ -28,6 +28,50 @@ export const mockAssistants: Assistant[] = [
},
];
+export let mockWorkflows: Workflow[] = [
+ {
+ id: 'wf1',
+ name: 'Lead Qualification Agent',
+ nodeCount: 11,
+ createdAt: '2024-03-01 10:00',
+ updatedAt: '2024-03-05 14:30',
+ nodes: [
+ {
+ name: "introduction",
+ type: "conversation",
+ isStart: true,
+ metadata: { position: { x: 100, y: 100 } },
+ prompt: "You are Morgan from GrowthPartners. Start with: 'Hello, this is Morgan from GrowthPartners. We help businesses improve their operational efficiency through custom software solutions. Do you have a few minutes to chat about how we might be able to help your business?' Use a friendly, consultative tone.",
+ messagePlan: { firstMessage: "Hello, this is Morgan from GrowthPartners. Do you have a few minutes to chat about how we might be able to help your business?" }
+ },
+ {
+ name: "need_discovery",
+ type: "conversation",
+ metadata: { position: { x: 400, y: 150 } },
+ prompt: "Conduct need discovery by asking about: 1) Their business and industry, 2) Current systems/processes they use, 3) Biggest challenges with current approach...",
+ variableExtractionPlan: {
+ output: [
+ { type: "string", title: "industry", description: "the user's industry or business type" },
+ { type: "string", title: "company_size", description: "approximate number of employees" }
+ ]
+ }
+ }
+ ],
+ edges: [
+ { from: "introduction", to: "need_discovery" }
+ ]
+ },
+ {
+ id: 'wf2',
+ name: '售后退款流程',
+ nodeCount: 5,
+ createdAt: '2024-03-01 10:00',
+ updatedAt: '2024-03-05 14:30',
+ nodes: [],
+ edges: []
+ },
+];
+
export const mockKnowledgeBases: KnowledgeBase[] = [
{
id: 'kb1',
@@ -85,8 +129,6 @@ export const mockVoices: Voice[] = [
{ id: 'v5', name: 'Doubao', vendor: 'Volcano', gender: 'Female', language: 'zh', description: 'Cute and young.' },
];
-// --- Dashboard Mock Data Helpers ---
-
export interface DashboardStats {
totalCalls: number;
answerRate: number;
@@ -96,14 +138,11 @@ export interface DashboardStats {
}
export const getDashboardStats = (timeRange: 'week' | 'month' | 'year', assistantId: string): DashboardStats => {
- // Simulate data variation based on inputs
const multiplier = assistantId === 'all' ? 1 : (assistantId === '1' ? 0.6 : 0.4);
const rangeMultiplier = timeRange === 'week' ? 1 : (timeRange === 'month' ? 4 : 52);
-
const baseCalls = Math.floor(100 * rangeMultiplier * multiplier);
- const transfers = Math.floor(baseCalls * 0.15); // 15% transfer rate
+ const transfers = Math.floor(baseCalls * 0.15);
- // Generate Trend Data
let points = 7;
if (timeRange === 'month') points = 30;
if (timeRange === 'year') points = 12;
@@ -122,7 +161,7 @@ export const getDashboardStats = (timeRange: 'week' | 'month' | 'year', assistan
return {
totalCalls: baseCalls,
- answerRate: 85 + Math.floor(Math.random() * 10), // 85-95%
+ answerRate: 85 + Math.floor(Math.random() * 10),
avgDuration: `${Math.floor(2 + Math.random() * 3)}m ${Math.floor(Math.random() * 60)}s`,
humanTransferCount: transfers,
trend
diff --git a/types.ts b/types.ts
index 1c23314..440b513 100644
--- a/types.ts
+++ b/types.ts
@@ -12,6 +12,15 @@ export interface Assistant {
hotwords: string[];
}
+export interface Voice {
+ id: string;
+ name: string;
+ vendor: string;
+ gender: string;
+ language: string;
+ description: string;
+}
+
export interface KnowledgeBase {
id: string;
name: string;
@@ -36,21 +45,54 @@ export interface CallLog {
agentName: string;
}
-export interface UserProfile {
- username: string;
- avatarUrl: string;
- email: string;
- language: 'zh' | 'en';
-}
-
-export interface Voice {
+export interface Workflow {
id: string;
name: string;
- vendor: 'Ali' | 'Volcano' | 'Minimax';
- gender: 'Male' | 'Female';
- language: 'zh' | 'en';
- description?: string;
- previewUrl?: string; // Mock url
+ nodeCount: number;
+ createdAt: string;
+ updatedAt: string;
+ nodes: WorkflowNode[];
+ edges: WorkflowEdge[];
+ globalPrompt?: string;
+}
+
+export interface WorkflowNode {
+ name: string;
+ type: 'conversation' | 'tool' | 'human' | 'end';
+ isStart?: boolean;
+ metadata: {
+ position: { x: number; y: number };
+ };
+ prompt?: string;
+ messagePlan?: {
+ firstMessage?: string;
+ };
+ variableExtractionPlan?: {
+ output: Array<{
+ type: string;
+ title: string;
+ description: string;
+ }>;
+ };
+ tool?: {
+ type: string;
+ function: {
+ name: string;
+ parameters: any;
+ };
+ destinations?: any[];
+ messages?: any[];
+ };
+ globalNodePlan?: {
+ enabled: boolean;
+ enterCondition: string;
+ };
+}
+
+export interface WorkflowEdge {
+ from: string;
+ to: string;
+ label?: string;
}
export enum TabValue {