WIP - using deep agent to create dog using workflow
This commit is contained in:
@@ -1,43 +1,153 @@
|
||||
import { Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState } from '@xyflow/react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import {
|
||||
Background,
|
||||
Controls,
|
||||
MiniMap,
|
||||
ReactFlow,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
addEdge,
|
||||
Connection,
|
||||
Edge,
|
||||
Node,
|
||||
Panel,
|
||||
} from '@xyflow/react'
|
||||
import '@xyflow/react/dist/style.css'
|
||||
import './styles.css'
|
||||
|
||||
const initialNodes = [
|
||||
{ id: 'start', data: { label: 'Start' }, position: { x: 0, y: 0 } },
|
||||
{ id: 'llm', data: { label: 'LLM Decision' }, position: { x: 220, y: 0 } },
|
||||
{ id: 'tool', data: { label: 'Tool Node' }, position: { x: 440, y: 0 } },
|
||||
{ id: 'end', data: { label: 'End' }, position: { x: 660, y: 0 } },
|
||||
const nodeTypes = {
|
||||
Start: { style: { background: '#22c55e', color: 'white', padding: 10, borderRadius: 5 } },
|
||||
LLMDecisionNode: { style: { background: '#3b82f6', color: 'white', padding: 10, borderRadius: 5 } },
|
||||
ToolNode: { style: { background: '#f59e0b', color: 'white', padding: 10, borderRadius: 5 } },
|
||||
HumanInputNode: { style: { background: '#8b5cf6', color: 'white', padding: 10, borderRadius: 5 } },
|
||||
End: { style: { background: '#ef4444', color: 'white', padding: 10, borderRadius: 5 } },
|
||||
}
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: 'start-1',
|
||||
type: 'default',
|
||||
data: { label: '🟢 Start', type: 'Start' },
|
||||
position: { x: 250, y: 50 },
|
||||
style: nodeTypes.Start.style,
|
||||
},
|
||||
{
|
||||
id: 'end-1',
|
||||
type: 'default',
|
||||
data: { label: '🔴 End', type: 'End' },
|
||||
position: { x: 250, y: 400 },
|
||||
style: nodeTypes.End.style,
|
||||
},
|
||||
]
|
||||
|
||||
const initialEdges = [
|
||||
{ id: 'e1-2', source: 'start', target: 'llm' },
|
||||
{ id: 'e2-3', source: 'llm', target: 'tool' },
|
||||
{ id: 'e3-4', source: 'tool', target: 'end' },
|
||||
]
|
||||
const initialEdges: Edge[] = []
|
||||
|
||||
export const App = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params: Connection) => setEdges((eds) => addEdge(params, eds)),
|
||||
[setEdges]
|
||||
)
|
||||
|
||||
// Send graph updates to parent window
|
||||
const notifyParent = useCallback(() => {
|
||||
const graphData = {
|
||||
id: 'process-graph',
|
||||
name: 'Process',
|
||||
nodes: nodes.map((node) => ({
|
||||
id: node.id,
|
||||
type: node.data.type || 'Start',
|
||||
position: node.position,
|
||||
data: node.data,
|
||||
})),
|
||||
edges: edges.map((edge) => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
condition: edge.data?.condition,
|
||||
})),
|
||||
}
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'GRAPH_UPDATED',
|
||||
payload: graphData,
|
||||
},
|
||||
'*'
|
||||
)
|
||||
}, [nodes, edges])
|
||||
|
||||
// Listen for graph load from parent
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data.type === 'LOAD_GRAPH') {
|
||||
const graph = event.data.payload
|
||||
if (graph && graph.nodes && graph.edges) {
|
||||
setNodes(
|
||||
graph.nodes.map((node: any) => ({
|
||||
id: node.id,
|
||||
type: 'default',
|
||||
data: { label: node.data.label || node.type, ...node.data },
|
||||
position: node.position,
|
||||
style: nodeTypes[node.type as keyof typeof nodeTypes]?.style || {},
|
||||
}))
|
||||
)
|
||||
setEdges(
|
||||
graph.edges.map((edge: any) => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
data: edge.condition ? { condition: edge.condition } : undefined,
|
||||
}))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage)
|
||||
return () => window.removeEventListener('message', handleMessage)
|
||||
}, [setNodes, setEdges])
|
||||
|
||||
// Notify parent on changes
|
||||
useEffect(() => {
|
||||
notifyParent()
|
||||
}, [nodes, edges, notifyParent])
|
||||
|
||||
const addNode = (type: string) => {
|
||||
const newNode: Node = {
|
||||
id: `${type.toLowerCase()}-${Date.now()}`,
|
||||
type: 'default',
|
||||
data: { label: `${type}`, type },
|
||||
position: { x: Math.random() * 400 + 50, y: Math.random() * 300 + 100 },
|
||||
style: nodeTypes[type as keyof typeof nodeTypes]?.style || {},
|
||||
}
|
||||
setNodes((nds) => nds.concat(newNode))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="editor-shell">
|
||||
<header className="editor-header">
|
||||
<h1>AI Process Builder</h1>
|
||||
<p>Design tenant workflows with deterministic execution.</p>
|
||||
</header>
|
||||
<div className="editor-canvas">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
fitView
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
fitView
|
||||
>
|
||||
<Panel position="top-left" className="node-palette">
|
||||
<h3>Node Palette</h3>
|
||||
<button onClick={() => addNode('LLMDecisionNode')}>🔵 LLM Decision</button>
|
||||
<button onClick={() => addNode('ToolNode')}>🟠 Tool</button>
|
||||
<button onClick={() => addNode('HumanInputNode')}>🟣 Human Input</button>
|
||||
<button onClick={() => addNode('End')}>🔴 End</button>
|
||||
</Panel>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,30 +4,68 @@ body {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.editor-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
.node-palette {
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.editor-header h1 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 18px;
|
||||
.node-palette h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.editor-header p {
|
||||
margin: 0;
|
||||
.node-palette button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 6px;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.node-palette button:hover {
|
||||
background: #f1f5f9;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.node-palette button:active {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.react-flow__node {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.editor-canvas {
|
||||
flex: 1;
|
||||
background: #f8fafc;
|
||||
.react-flow__edge-path {
|
||||
stroke: #64748b;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.react-flow__edge.selected .react-flow__edge-path {
|
||||
stroke: #3b82f6;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,16 +7,31 @@ import {
|
||||
InputGroupText,
|
||||
} from '@/components/ui/input-group'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { ArrowUp } from 'lucide-vue-next'
|
||||
import { ArrowUp, Loader2 } from 'lucide-vue-next'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useApi } from '@/composables/useApi'
|
||||
|
||||
interface ChatMessage {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
text: string;
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
|
||||
interface StreamEvent {
|
||||
type: string;
|
||||
data?: any;
|
||||
processId?: string;
|
||||
nodeId?: string;
|
||||
toolName?: string;
|
||||
}
|
||||
|
||||
const chatInput = ref('')
|
||||
const messages = ref<{ role: 'user' | 'assistant'; text: string }[]>([])
|
||||
const messages = ref<ChatMessage[]>([])
|
||||
const sending = ref(false)
|
||||
const route = useRoute()
|
||||
const { api } = useApi()
|
||||
const sessionId = ref<string | null>(null)
|
||||
const eventSource = ref<EventSource | null>(null)
|
||||
|
||||
const getTenantId = () => {
|
||||
if (!import.meta.client) return 'tenant1'
|
||||
@@ -39,6 +54,97 @@ const buildContext = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const connectToStream = (sessionIdValue: string) => {
|
||||
if (eventSource.value) {
|
||||
eventSource.value.close()
|
||||
}
|
||||
|
||||
const baseUrl = window.location.hostname === 'localhost'
|
||||
? 'http://localhost:3000'
|
||||
: `https://${window.location.hostname}`
|
||||
|
||||
eventSource.value = new EventSource(
|
||||
`${baseUrl}/tenants/${getTenantId()}/ai-chat/stream?sessionId=${sessionIdValue}`
|
||||
)
|
||||
|
||||
eventSource.value.onmessage = (event) => {
|
||||
try {
|
||||
const payload: StreamEvent = JSON.parse(event.data)
|
||||
handleStreamEvent(payload)
|
||||
} catch (error) {
|
||||
console.error('Failed to parse stream event:', error)
|
||||
}
|
||||
}
|
||||
|
||||
eventSource.value.onerror = () => {
|
||||
eventSource.value?.close()
|
||||
eventSource.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const handleStreamEvent = (event: StreamEvent) => {
|
||||
switch (event.type) {
|
||||
case 'agent_started':
|
||||
// Agent is thinking
|
||||
break
|
||||
case 'processes_listed':
|
||||
// Processes discovered
|
||||
break
|
||||
case 'process_selected':
|
||||
messages.value.push({
|
||||
role: 'system',
|
||||
text: `🔄 Selected process: ${event.data?.processName || 'Process'}`,
|
||||
})
|
||||
break
|
||||
case 'agent_message':
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
text: event.data?.message || '',
|
||||
})
|
||||
break
|
||||
case 'node_started':
|
||||
const lastMsg = messages.value[messages.value.length - 1]
|
||||
if (lastMsg?.isStreaming) {
|
||||
lastMsg.text += `\n⚙️ Executing step...`
|
||||
}
|
||||
break
|
||||
case 'tool_called':
|
||||
const lastToolMsg = messages.value[messages.value.length - 1]
|
||||
if (lastToolMsg?.isStreaming) {
|
||||
lastToolMsg.text += `\n🔧 Using tool: ${event.toolName}`
|
||||
}
|
||||
break
|
||||
case 'need_input':
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
text: event.data?.prompt || 'I need some additional information from you.',
|
||||
})
|
||||
sending.value = false
|
||||
break
|
||||
case 'final':
|
||||
if (event.data?.output) {
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
text: event.data.message || '✅ Process completed successfully!',
|
||||
})
|
||||
} else if (event.data?.reply) {
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
text: event.data.reply,
|
||||
})
|
||||
}
|
||||
sending.value = false
|
||||
break
|
||||
case 'error':
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
text: `❌ Error: ${event.data?.error || 'An error occurred'}`,
|
||||
})
|
||||
sending.value = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!chatInput.value.trim()) return
|
||||
|
||||
@@ -47,8 +153,20 @@ const handleSend = async () => {
|
||||
chatInput.value = ''
|
||||
sending.value = true
|
||||
|
||||
// Add a streaming message placeholder
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
text: '🤔 Thinking...',
|
||||
isStreaming: true
|
||||
})
|
||||
|
||||
try {
|
||||
const history = messages.value.slice(0, -1).slice(-6)
|
||||
const history = messages.value
|
||||
.filter(m => m.role !== 'system' && !m.isStreaming)
|
||||
.slice(0, -1)
|
||||
.slice(-6)
|
||||
.map(m => ({ role: m.role, text: m.text }))
|
||||
|
||||
const response = await api.post(`/tenants/${getTenantId()}/ai-chat/messages`, {
|
||||
message,
|
||||
history,
|
||||
@@ -56,34 +174,32 @@ const handleSend = async () => {
|
||||
sessionId: sessionId.value || undefined,
|
||||
})
|
||||
|
||||
if (response.sessionId) {
|
||||
if (response.sessionId && !sessionId.value) {
|
||||
sessionId.value = response.sessionId
|
||||
connectToStream(response.sessionId)
|
||||
}
|
||||
|
||||
// Remove streaming placeholder and add response
|
||||
messages.value = messages.value.filter(m => !m.isStreaming)
|
||||
|
||||
if (response.reply) {
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
text: response.reply,
|
||||
})
|
||||
if (response.action === 'create_record') {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('ai-record-created', {
|
||||
detail: {
|
||||
objectApiName: buildContext().objectApiName,
|
||||
record: response.record,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
text: 'Process started. I will post updates as soon as they are ready.',
|
||||
})
|
||||
// If process is running, stream will handle updates
|
||||
if (response.runId) {
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
text: '⏳ Processing workflow...',
|
||||
isStreaming: true,
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to send AI chat message:', error)
|
||||
messages.value = messages.value.filter(m => !m.isStreaming)
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
text: error.message || 'Sorry, I ran into an error. Please try again.',
|
||||
@@ -92,11 +208,17 @@ const handleSend = async () => {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (eventSource.value) {
|
||||
eventSource.value.close()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ai-chat-area w-full border-t border-border p-4 bg-neutral-50">
|
||||
<div class="ai-chat-messages mb-4 space-y-3">
|
||||
<div class="ai-chat-messages mb-4 space-y-3 max-h-[400px] overflow-y-auto">
|
||||
<div
|
||||
v-for="(message, index) in messages"
|
||||
:key="`${message.role}-${index}`"
|
||||
@@ -104,14 +226,19 @@ const handleSend = async () => {
|
||||
:class="message.role === 'user' ? 'justify-end' : 'justify-start'"
|
||||
>
|
||||
<div
|
||||
class="max-w-[80%] rounded-lg px-3 py-2 text-sm"
|
||||
:class="message.role === 'user' ? 'bg-primary text-primary-foreground' : 'bg-white border border-border text-foreground'"
|
||||
class="max-w-[80%] rounded-lg px-3 py-2 text-sm whitespace-pre-line"
|
||||
:class="{
|
||||
'bg-primary text-primary-foreground': message.role === 'user',
|
||||
'bg-white border border-border text-foreground': message.role === 'assistant',
|
||||
'bg-blue-50 border border-blue-200 text-blue-900 text-xs': message.role === 'system',
|
||||
}"
|
||||
>
|
||||
<Loader2 v-if="message.isStreaming" class="inline-block size-3 animate-spin mr-1" />
|
||||
{{ message.text }}
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="messages.length === 0" class="text-sm text-muted-foreground">
|
||||
Ask the assistant to add records, filter lists, or summarize the page.
|
||||
Ask the assistant to execute business processes, add records, or answer questions.
|
||||
</p>
|
||||
</div>
|
||||
<InputGroup>
|
||||
|
||||
192
frontend/pages/ai-processes/[id]/edit.vue
Normal file
192
frontend/pages/ai-processes/[id]/edit.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
import { Save, Play, ArrowLeft } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useApi } from '@/composables/useApi'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
interface ProcessGraph {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
nodes: any[]
|
||||
edges: any[]
|
||||
}
|
||||
|
||||
const { api } = useApi()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const processId = computed(() => route.params.id === 'new' ? null : String(route.params.id))
|
||||
|
||||
const processName = ref('')
|
||||
const processDescription = ref('')
|
||||
const graphDefinition = ref<ProcessGraph | null>(null)
|
||||
const saving = ref(false)
|
||||
const loading = ref(true)
|
||||
|
||||
const getTenantId = () => {
|
||||
if (!import.meta.client) return 'tenant1'
|
||||
return localStorage.getItem('tenantId') || 'tenant1'
|
||||
}
|
||||
|
||||
const loadProcess = async () => {
|
||||
if (!processId.value) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get(`/tenants/${getTenantId()}/ai-processes/${processId.value}`)
|
||||
processName.value = data.name
|
||||
processDescription.value = data.description || ''
|
||||
|
||||
if (data.versions && data.versions.length > 0) {
|
||||
const latestVersion = data.versions[data.versions.length - 1]
|
||||
graphDefinition.value = latestVersion.graphJson
|
||||
|
||||
// Send graph to iframe
|
||||
sendGraphToEditor(latestVersion.graphJson)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load process:', error)
|
||||
alert('Failed to load process')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const sendGraphToEditor = (graph: any) => {
|
||||
const iframe = document.getElementById('process-editor-iframe') as HTMLIFrameElement
|
||||
if (iframe && iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'LOAD_GRAPH',
|
||||
payload: graph
|
||||
}, '*')
|
||||
}
|
||||
}
|
||||
|
||||
const saveProcess = async () => {
|
||||
if (!processName.value.trim()) {
|
||||
alert('Process name is required')
|
||||
return
|
||||
}
|
||||
|
||||
if (!graphDefinition.value) {
|
||||
alert('Please design the process graph first')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
if (processId.value) {
|
||||
// Update existing process (create new version)
|
||||
await api.post(`/tenants/${getTenantId()}/ai-processes/${processId.value}/versions`, {
|
||||
graph: graphDefinition.value
|
||||
})
|
||||
alert('Process version saved successfully!')
|
||||
} else {
|
||||
// Create new process
|
||||
const result = await api.post(`/tenants/${getTenantId()}/ai-processes`, {
|
||||
name: processName.value,
|
||||
description: processDescription.value,
|
||||
graph: graphDefinition.value
|
||||
})
|
||||
alert('Process created successfully!')
|
||||
router.push(`/ai-processes/${result.id}/edit`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to save process:', error)
|
||||
alert(`Save failed: ${error.message || 'Unknown error'}`)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testRun = async () => {
|
||||
if (!processId.value) {
|
||||
alert('Please save the process first')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await api.post(`/tenants/${getTenantId()}/ai-processes/${processId.value}/runs`, {
|
||||
input: { test: true }
|
||||
})
|
||||
console.log('Test run result:', result)
|
||||
alert('Test run started! Check the console for details.')
|
||||
} catch (error: any) {
|
||||
alert(`Test failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditorMessage = (event: MessageEvent) => {
|
||||
if (event.data.type === 'GRAPH_UPDATED') {
|
||||
graphDefinition.value = event.data.payload
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProcess()
|
||||
window.addEventListener('message', handleEditorMessage)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('message', handleEditorMessage)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-screen flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="border-b bg-background px-6 py-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4 flex-1">
|
||||
<Button variant="ghost" size="icon" @click="router.push('/ai-processes')">
|
||||
<ArrowLeft class="h-4 w-4" />
|
||||
</Button>
|
||||
<div class="flex-1 max-w-md">
|
||||
<Input
|
||||
v-model="processName"
|
||||
placeholder="Process Name"
|
||||
class="font-semibold"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" @click="testRun" :disabled="!processId">
|
||||
<Play class="h-4 w-4 mr-2" />
|
||||
Test Run
|
||||
</Button>
|
||||
<Button @click="saveProcess" :disabled="saving">
|
||||
<Save class="h-4 w-4 mr-2" />
|
||||
{{ saving ? 'Saving...' : 'Save' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process Description -->
|
||||
<div class="border-b bg-background px-6 py-2">
|
||||
<Textarea
|
||||
v-model="processDescription"
|
||||
placeholder="Process description (optional)"
|
||||
class="resize-none text-sm"
|
||||
rows="2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Editor Iframe -->
|
||||
<div class="flex-1 relative">
|
||||
<div v-if="loading" class="absolute inset-0 flex items-center justify-center bg-background/80">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
<iframe
|
||||
id="process-editor-iframe"
|
||||
src="/ai-processes-editor/index.html"
|
||||
class="w-full h-full border-0"
|
||||
title="Process Editor"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
133
frontend/pages/ai-processes/index.vue
Normal file
133
frontend/pages/ai-processes/index.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
import { Plus, Workflow, Play } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useApi } from '@/composables/useApi'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
interface Process {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
latestVersion: number
|
||||
createdAt: string
|
||||
versions?: any[]
|
||||
}
|
||||
|
||||
const { api } = useApi()
|
||||
const router = useRouter()
|
||||
const processes = ref<Process[]>([])
|
||||
const loading = ref(true)
|
||||
|
||||
const getTenantId = () => {
|
||||
if (!import.meta.client) return 'tenant1'
|
||||
return localStorage.getItem('tenantId') || 'tenant1'
|
||||
}
|
||||
|
||||
const loadProcesses = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get(`/tenants/${getTenantId()}/ai-processes`)
|
||||
processes.value = data || []
|
||||
} catch (error) {
|
||||
console.error('Failed to load processes:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createNewProcess = () => {
|
||||
router.push('/ai-processes/new')
|
||||
}
|
||||
|
||||
const editProcess = (processId: string) => {
|
||||
router.push(`/ai-processes/${processId}/edit`)
|
||||
}
|
||||
|
||||
const testProcess = async (processId: string) => {
|
||||
try {
|
||||
const result = await api.post(`/tenants/${getTenantId()}/ai-processes/${processId}/runs`, {
|
||||
input: { test: true },
|
||||
})
|
||||
console.log('Test run result:', result)
|
||||
alert('Process test started! Check console for details.')
|
||||
} catch (error: any) {
|
||||
alert(`Test failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProcesses()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mx-auto p-6 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">AI Process Builder</h1>
|
||||
<p class="text-muted-foreground mt-1">
|
||||
Create and manage intelligent business process workflows
|
||||
</p>
|
||||
</div>
|
||||
<Button @click="createNewProcess">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
New Process
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="processes.length === 0" class="text-center py-12">
|
||||
<Workflow class="mx-auto h-12 w-12 text-muted-foreground/50" />
|
||||
<h3 class="mt-4 text-lg font-semibold">No processes yet</h3>
|
||||
<p class="text-muted-foreground mt-1">
|
||||
Get started by creating your first AI-powered business process
|
||||
</p>
|
||||
<Button @click="createNewProcess" class="mt-4">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Create Process
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card
|
||||
v-for="process in processes"
|
||||
:key="process.id"
|
||||
class="hover:shadow-lg transition-shadow cursor-pointer"
|
||||
@click="editProcess(process.id)"
|
||||
>
|
||||
<CardHeader>
|
||||
<div class="flex items-start justify-between">
|
||||
<Workflow class="h-5 w-5 text-primary" />
|
||||
<Badge variant="secondary">v{{ process.latestVersion }}</Badge>
|
||||
</div>
|
||||
<CardTitle class="mt-2">{{ process.name }}</CardTitle>
|
||||
<CardDescription>{{ process.description || 'No description' }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click.stop="editProcess(process.id)"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@click.stop="testProcess(process.id)"
|
||||
>
|
||||
<Play class="h-3 w-3 mr-1" />
|
||||
Test
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user