Added functional chat for simply adding blocks and connections

This commit is contained in:
Emir Karabeg
2025-02-06 19:51:11 -08:00
parent c415467db4
commit f42882e3ae
9 changed files with 654 additions and 12 deletions
+208
View File
@@ -0,0 +1,208 @@
import { OpenAI } from 'openai'
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { ChatCompletionMessageParam } from 'openai/resources/chat/completions'
// Validation schemas
const MessageSchema = z.object({
role: z.enum(['user', 'assistant', 'system']),
content: z.string()
})
const RequestSchema = z.object({
messages: z.array(MessageSchema),
workflowState: z.object({
blocks: z.record(z.any()),
edges: z.array(z.any())
})
})
// Define function schemas with strict typing
const workflowActions = {
addBlock: {
description: "Add one new block to the workflow",
parameters: {
type: "object",
required: ["type"],
properties: {
type: {
type: "string",
enum: ["agent", "api", "condition", "function", "router"],
description: "The type of block to add"
},
name: {
type: "string",
description: "Optional custom name for the block. Do not provide a name unless the user has specified it."
},
position: {
type: "object",
description: "Optional position for the block. Do not provide a position unless the user has specified it.",
properties: {
x: { type: "number" },
y: { type: "number" }
}
}
}
}
},
addEdge: {
description: "Create a connection between two blocks",
parameters: {
type: "object",
required: ["sourceId", "targetId"],
properties: {
sourceId: {
type: "string",
description: "ID of the source block"
},
targetId: {
type: "string",
description: "ID of the target block"
},
sourceHandle: {
type: "string",
description: "Optional handle identifier for the source connection point"
},
targetHandle: {
type: "string",
description: "Optional handle identifier for the target connection point"
}
}
}
},
removeBlock: {
description: "Remove a block from the workflow",
parameters: {
type: "object",
required: ["id"],
properties: {
id: { type: "string", description: "ID of the block to remove" }
}
}
},
removeEdge: {
description: "Remove a connection between blocks",
parameters: {
type: "object",
required: ["id"],
properties: {
id: { type: "string", description: "ID of the edge to remove" }
}
}
}
}
// System prompt that references workflow state
const getSystemPrompt = (workflowState: any) => {
const blockCount = Object.keys(workflowState.blocks).length
const edgeCount = workflowState.edges.length
// Create a summary of existing blocks
const blockSummary = Object.values(workflowState.blocks)
.map((block: any) => `- ${block.type} block named "${block.name}" with id ${block.id}`)
.join('\n')
// Create a summary of existing edges
const edgeSummary = workflowState.edges
.map((edge: any) => `- ${edge.source} -> ${edge.target} with id ${edge.id}`)
.join('\n')
return `You are a workflow assistant that helps users modify their workflow by adding/removing blocks and connections.
Current Workflow State:
${blockCount === 0 ? 'The workflow is empty.' : `${blockSummary}
Connections:
${edgeCount === 0 ? 'No connections between blocks.' : edgeSummary}`}
When users request changes:
- Consider existing blocks when suggesting connections
- Provide clear feedback about what actions you've taken
Use the following functions to modify the workflow:
1. Use the addBlock function to create new blocks
2. Use the addEdge function to connect blocks
3. Use the removeBlock function to remove blocks
4. Use the removeEdge function to remove connections
Only use the provided functions and respond naturally to the user's requests.`
}
export async function POST(request: Request) {
try {
// Validate API key
const apiKey = request.headers.get('X-OpenAI-Key')
if (!apiKey) {
return NextResponse.json(
{ error: 'OpenAI API key is required' },
{ status: 401 }
)
}
// Parse and validate request body
const body = await request.json()
const validatedData = RequestSchema.parse(body)
const { messages, workflowState } = validatedData
// Initialize OpenAI client
const openai = new OpenAI({ apiKey })
// Create message history with workflow context
const messageHistory = [
{ role: 'system', content: getSystemPrompt(workflowState) },
...messages
]
// Make OpenAI API call with workflow context
const completion = await openai.chat.completions.create({
model: "gpt-4o",
messages: messageHistory as ChatCompletionMessageParam[],
tools: Object.entries(workflowActions).map(([name, config]) => ({
type: 'function',
function: {
name,
description: config.description,
parameters: config.parameters
}
})),
tool_choice: "auto"
})
const message = completion.choices[0].message
// Process tool calls if present
if (message.tool_calls) {
console.log(message.tool_calls)
const actions = message.tool_calls.map(call => ({
name: call.function.name,
parameters: JSON.parse(call.function.arguments)
}))
return NextResponse.json({
message: message.content || "I've updated the workflow based on your request.",
actions
})
}
// Return response with no actions
return NextResponse.json({
message: message.content || "I'm not sure what changes to make to the workflow. Can you please provide more specific instructions?"
})
} catch (error) {
console.error('Chat API error:', error)
// Handle specific error types
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid request format', details: error.errors },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Failed to process chat message' },
{ status: 500 }
)
}
}
+7 -4
View File
@@ -5,16 +5,19 @@ import { MessageCircle, Send, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useAIChatStore } from '@/stores/chat/store'
export function Chat() {
const { messages, isProcessing, error, sendMessage } = useAIChatStore()
const [isOpen, setIsOpen] = useState(false)
const [message, setMessage] = useState('')
const handleSubmit = (e: React.FormEvent) => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// Handle message submission here
if (!message.trim()) return
await sendMessage(message)
setMessage('')
setIsOpen(false) // Close the chat after sending
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
@@ -42,7 +45,7 @@ export function Chat() {
}
return (
<div className="fixed bottom-16 left-1/2 -translate-x-1/2 z-50 w-[500px] bg-background rounded-2xl border shadow-lg">
<div className="fixed bottom-16 left-1/2 -translate-x-1/2 z-50 w-[50%] max-w-[500px] min-w-[280px] bg-background rounded-2xl border shadow-lg">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Button
variant="ghost"
+260 -4
View File
@@ -27,13 +27,15 @@
"date-fns": "^4.1.0",
"lucide-react": "^0.469.0",
"next": "15.1.3",
"openai": "^4.83.0",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-simple-code-editor": "^0.14.1",
"reactflow": "^11.11.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.1"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
@@ -3497,12 +3499,21 @@
"version": "20.17.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.11.tgz",
"integrity": "sha512-Ept5glCK35R8yeyIeYlRIZtX6SLRyqMhOFTgj5SOkMpLTdw3SEHI9fHx60xaUZ+V1aJxQJODE+7/j5ocZydYTg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/@types/prismjs": {
"version": "1.26.5",
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
@@ -3562,6 +3573,30 @@
"dev": true,
"license": "MIT"
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/agentkeepalive": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
"license": "MIT",
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -3656,6 +3691,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -4780,6 +4821,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -5016,6 +5069,15 @@
"node": ">=0.10.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
@@ -5176,6 +5238,15 @@
"node": ">=4"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
@@ -5359,6 +5430,39 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data-encoder": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
"license": "MIT"
},
"node_modules/formdata-node": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"license": "MIT",
"dependencies": {
"node-domexception": "1.0.0",
"web-streams-polyfill": "4.0.0-beta.3"
},
"engines": {
"node": ">= 12.20"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -5542,6 +5646,15 @@
"node": ">=10.17.0"
}
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.0.0"
}
},
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
@@ -7099,6 +7212,27 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@@ -7150,7 +7284,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/mz": {
@@ -7279,6 +7412,45 @@
"optional": true,
"peer": true
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -7359,6 +7531,51 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openai": {
"version": "4.83.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.83.0.tgz",
"integrity": "sha512-fmTsqud0uTtRKsPC7L8Lu55dkaTwYucqncDHzVvO64DKOpNTuiYwjbR/nVgpapXuYy8xSnhQQPUm+3jQaxICgw==",
"license": "Apache-2.0",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7"
},
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/openai/node_modules/@types/node": {
"version": "18.19.75",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.75.tgz",
"integrity": "sha512-UIksWtThob6ZVSyxcOqCLOUNg/dyO1Qvx4McgeuhrEtHTLFTf7BBhEazaE4K806FGTPtzd/2sE90qn4fVr7cyw==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/openai/node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT"
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -8837,6 +9054,12 @@
"node": ">=8.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@@ -8939,7 +9162,6 @@
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": {
@@ -9056,6 +9278,31 @@
"makeerror": "1.0.12"
}
},
"node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -9306,6 +9553,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zustand": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
+3 -1
View File
@@ -33,13 +33,15 @@
"date-fns": "^4.1.0",
"lucide-react": "^0.469.0",
"next": "15.1.3",
"openai": "^4.83.0",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-simple-code-editor": "^0.14.1",
"reactflow": "^11.11.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.1"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
+142
View File
@@ -0,0 +1,142 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { useWorkflowStore } from '../workflow/store'
import { useEnvironmentStore } from '../environment/store'
import { AIChatStore, ChatMessage } from './types'
import { getNextBlockNumber } from './utils'
export const useAIChatStore = create<AIChatStore>()(
devtools(
(set, get) => ({
messages: [],
isProcessing: false,
error: null,
sendMessage: async (content: string) => {
try {
set({ isProcessing: true, error: null })
const workflowStore = useWorkflowStore.getState()
const apiKey = useEnvironmentStore.getState().getVariable('OPENAI_API_KEY')
if (!apiKey) {
throw new Error('OpenAI API key not found. Please add it to your environment variables.')
}
// User message
const newMessage: ChatMessage = {
id: crypto.randomUUID(),
role: 'user',
content: content.trim(),
timestamp: Date.now(),
}
// Format messages for OpenAI API
const formattedMessages = [
...get().messages.map(msg => ({
role: msg.role,
content: msg.content
})),
{
role: newMessage.role,
content: newMessage.content
}
]
// Add message to local state first
set(state => ({
messages: [...state.messages, newMessage]
}))
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-OpenAI-Key': apiKey
},
body: JSON.stringify({
messages: formattedMessages,
workflowState: {
blocks: workflowStore.blocks,
edges: workflowStore.edges
}
})
})
if (!response.ok) {
throw new Error('Failed to send message')
}
const data = await response.json()
console.log('OPENAI RESPONSE', data)
// Handle any actions returned from the API
if (data.actions) {
for (const action of data.actions) {
switch (action.name) {
case 'addBlock': {
const { type, name, position } = action.parameters
const id = crypto.randomUUID()
const defaultPosition = position || {
x: Object.keys(workflowStore.blocks).length * 250,
y: 100
}
// Generate name if not provided
const blockName = name || `${type} ${getNextBlockNumber(workflowStore.blocks, type)}`
workflowStore.addBlock(id, type, blockName, defaultPosition)
break
}
case 'addEdge': {
const { sourceId, targetId, sourceHandle, targetHandle } = action.parameters
workflowStore.addEdge({
id: crypto.randomUUID(),
source: sourceId,
target: targetId,
sourceHandle,
targetHandle,
type: 'custom'
})
break
}
case 'removeBlock': {
workflowStore.removeBlock(action.parameters.id)
break
}
case 'removeEdge': {
workflowStore.removeEdge(action.parameters.id)
break
}
}
}
}
// Add assistant's response to chat
if (data.message) {
set(state => ({
messages: [...state.messages, {
id: crypto.randomUUID(),
role: 'assistant',
content: data.message,
timestamp: Date.now()
}]
}))
}
} catch (error) {
console.error('Chat error:', error)
set({ error: error instanceof Error ? error.message : 'Unknown error' })
} finally {
set({ isProcessing: false })
}
},
clearChat: () => set({ messages: [], error: null }),
setError: (error) => set({ error })
}),
{ name: 'chat-store' }
)
)
+20
View File
@@ -0,0 +1,20 @@
export interface ChatMessage {
id: string
role: 'user' | 'assistant' | 'system'
content: string
timestamp: number
}
export interface AIChatState {
messages: ChatMessage[]
isProcessing: boolean
error: string | null
}
export interface AIChatActions {
sendMessage: (content: string) => Promise<void>
clearChat: () => void
setError: (error: string | null) => void
}
export type AIChatStore = AIChatState & AIChatActions
+12
View File
@@ -0,0 +1,12 @@
// Helper function to get the next block number for a given type
export const getNextBlockNumber = (blocks: Record<string, any>, type: string) => {
const typeBlocks = Object.values(blocks)
.filter((block: any) => block.type.toLowerCase() === type.toLowerCase())
.map((block: any) => {
const match = block.name.match(new RegExp(`${type}\\s*(\\d+)`, 'i'))
return match ? parseInt(match[1]) : 0
})
const maxNumber = Math.max(0, ...typeBlocks)
return maxNumber + 1
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { devtools } from 'zustand/middleware'
import { getBlock } from '@/blocks'
import { resolveOutputType } from '@/blocks/utils'
import { WorkflowStoreWithHistory, pushHistory, withHistory } from './middleware'
import { Position, SubBlockState, WorkflowStore } from './types'
import { Position, SubBlockState } from './types'
const initialState = {
blocks: {},
+1 -2
View File
@@ -1,6 +1,5 @@
import { Edge, Node } from 'reactflow'
import { Edge } from 'reactflow'
import { BlockOutput, SubBlockType } from '@/blocks/types'
import { WorkflowHistory } from './history-types'
export interface Position {
x: number