From bac7f11fda937d596295619365dfab5d88878bf9 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 07:57:32 +0200 Subject: [PATCH 01/42] page --- src/dashboard/Dashboard.js | 2 + src/dashboard/DashboardView.react.js | 5 + src/dashboard/Data/Agent/Agent.react.js | 181 ++++++++++++++++++++++ src/dashboard/Data/Agent/Agent.scss | 194 ++++++++++++++++++++++++ src/icons/sparkle.svg | 5 + 5 files changed, 387 insertions(+) create mode 100644 src/dashboard/Data/Agent/Agent.react.js create mode 100644 src/dashboard/Data/Agent/Agent.scss create mode 100644 src/icons/sparkle.svg diff --git a/src/dashboard/Dashboard.js b/src/dashboard/Dashboard.js index b1e8a800bd..cddde075ad 100644 --- a/src/dashboard/Dashboard.js +++ b/src/dashboard/Dashboard.js @@ -7,6 +7,7 @@ */ import AccountOverview from './Account/AccountOverview.react'; import AccountView from './AccountView.react'; +import Agent from './Data/Agent/Agent.react'; import AnalyticsOverview from './Analytics/Overview/Overview.react'; import ApiConsole from './Data/ApiConsole/ApiConsole.react'; import AppData from './AppData.react'; @@ -273,6 +274,7 @@ export default class Dashboard extends React.Component { } /> } /> } /> + } /> } /> {JobsRoute} diff --git a/src/dashboard/DashboardView.react.js b/src/dashboard/DashboardView.react.js index c48e5ac461..e49751d54d 100644 --- a/src/dashboard/DashboardView.react.js +++ b/src/dashboard/DashboardView.react.js @@ -81,6 +81,11 @@ export default class DashboardView extends React.Component { link: '/views', }); + coreSubsections.push({ + name: '✨ Agent', + link: '/agent', + }); + //webhooks requires removal of heroku link code, then it should work. if ( features.hooks && diff --git a/src/dashboard/Data/Agent/Agent.react.js b/src/dashboard/Data/Agent/Agent.react.js new file mode 100644 index 0000000000..4b07a148fe --- /dev/null +++ b/src/dashboard/Data/Agent/Agent.react.js @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2016-present, Parse, LLC + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ +import DashboardView from 'dashboard/DashboardView.react'; +import EmptyState from 'components/EmptyState/EmptyState.react'; +import React from 'react'; +import SidebarAction from 'components/Sidebar/SidebarAction'; +import Toolbar from 'components/Toolbar/Toolbar.react'; +import styles from './Agent.scss'; +import { withRouter } from 'lib/withRouter'; + +@withRouter +class Agent extends DashboardView { + constructor() { + super(); + this.section = 'Core'; + this.subsection = 'Agent'; + + this.state = { + messages: [], + inputValue: '', + isLoading: false, + }; + + this.action = new SidebarAction('Clear Chat', () => this.clearChat()); + } + + clearChat() { + this.setState({ messages: [] }); + } + + handleInputChange = (event) => { + this.setState({ inputValue: event.target.value }); + } + + handleSubmit = (event) => { + event.preventDefault(); + const { inputValue } = this.state; + + if (inputValue.trim() === '') { + return; + } + + // Add user message + const userMessage = { + id: Date.now(), + type: 'user', + content: inputValue.trim(), + timestamp: new Date(), + }; + + this.setState(prevState => ({ + messages: [...prevState.messages, userMessage], + inputValue: '', + isLoading: true, + })); + + // Simulate AI response (replace with actual AI integration later) + setTimeout(() => { + const aiMessage = { + id: Date.now() + 1, + type: 'agent', + content: `I received your message: "${inputValue.trim()}". This is a placeholder response. AI integration will be implemented here.`, + timestamp: new Date(), + }; + + this.setState(prevState => ({ + messages: [...prevState.messages, aiMessage], + isLoading: false, + })); + }, 1000); + } + + renderToolbar() { + return ( + + + + ); + } + + renderMessages() { + const { messages, isLoading } = this.state; + + if (messages.length === 0) { + return null; // Empty state is now handled as overlay + } + + return ( +
+ {messages.map((message) => ( +
+
+ {message.content} +
+
+ {message.timestamp.toLocaleTimeString()} +
+
+ ))} + {isLoading && ( +
+
+
+ + + +
+
+
+ )} +
+ ); + } + + renderChatInput() { + const { inputValue, isLoading } = this.state; + + return ( +
+
+ + +
+
+ ); + } + + renderContent() { + const { messages } = this.state; + + return ( +
+ {this.renderToolbar()} +
+
+ {this.renderMessages()} +
+ {this.renderChatInput()} +
+ {messages.length === 0 && ( +
+ +
+ )} +
+ ); + } +} + +export default Agent; diff --git a/src/dashboard/Data/Agent/Agent.scss b/src/dashboard/Data/Agent/Agent.scss new file mode 100644 index 0000000000..172ac1f34d --- /dev/null +++ b/src/dashboard/Data/Agent/Agent.scss @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2016-present, Parse, LLC + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +.agentContainer { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.chatContainer { + display: flex; + flex-direction: column; + flex: 1; + min-height: calc(100vh - 60px); /* Account for toolbar */ + overflow: hidden; +} + +.chatWindow { + flex: 1; + overflow-y: auto; + padding: 20px; + background-color: #f8f9fa; +} + +.emptyStateOverlay { + position: fixed; + left: 300px; + top: 0; + bottom: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; /* Allow clicks to pass through to the input below */ + z-index: 10; +} + +.emptyStateOverlay > * { + pointer-events: auto; /* Re-enable pointer events for the actual content */ +} + +.messagesContainer { + display: flex; + flex-direction: column; + gap: 16px; +} + +.message { + max-width: 70%; + padding: 12px 16px; + border-radius: 18px; + margin-bottom: 8px; + position: relative; + word-wrap: break-word; +} + +.message.user { + align-self: flex-end; + background-color: #007bff; + color: white; + margin-left: auto; +} + +.message.agent { + align-self: flex-start; + background-color: white; + color: #333; + border: 1px solid #e1e5e9; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.messageContent { + font-size: 14px; + line-height: 1.4; +} + +.messageTime { + font-size: 11px; + opacity: 0.7; + margin-top: 4px; + text-align: right; +} + +.message.agent .messageTime { + text-align: left; +} + +.typing { + display: flex; + gap: 4px; + align-items: center; +} + +.typing span { + width: 6px; + height: 6px; + border-radius: 50%; + background-color: #999; + animation: typing 1.4s infinite ease-in-out; +} + +.typing span:nth-child(1) { + animation-delay: -0.32s; +} + +.typing span:nth-child(2) { + animation-delay: -0.16s; +} + +@keyframes typing { + 0%, 80%, 100% { + transform: scale(0); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } +} + +.chatForm { + background-color: white; + border-top: 1px solid #e1e5e9; + padding: 16px 20px; +} + +.inputContainer { + display: flex; + gap: 12px; + align-items: center; +} + +.chatInput { + flex: 1; + padding: 12px 16px; + border: 1px solid #e1e5e9; + border-radius: 24px; + font-size: 14px; + outline: none; + resize: none; + transition: border-color 0.2s ease; +} + +.chatInput:focus { + border-color: #007bff; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); +} + +.chatInput:disabled { + background-color: #f8f9fa; + color: #6c757d; +} + +.sendButton { + padding: 12px 24px; + background-color: #007bff; + color: white; + border: none; + border-radius: 24px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease; + min-width: 80px; +} + +.sendButton:hover:not(:disabled) { + background-color: #0056b3; +} + +.sendButton:disabled { + background-color: #6c757d; + cursor: not-allowed; +} + +.clearButton { + padding: 8px 16px; + background-color: #dc3545; + color: white; + border: none; + border-radius: 4px; + font-size: 13px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.clearButton:hover { + background-color: #c82333; +} diff --git a/src/icons/sparkle.svg b/src/icons/sparkle.svg new file mode 100644 index 0000000000..17b72ebcf4 --- /dev/null +++ b/src/icons/sparkle.svg @@ -0,0 +1,5 @@ + + + + + From 19cdc0166970b3d5b0e94fc6fa0e8fd220362688 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 07:58:36 +0200 Subject: [PATCH 02/42] Update Agent.scss --- src/dashboard/Data/Agent/Agent.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/dashboard/Data/Agent/Agent.scss b/src/dashboard/Data/Agent/Agent.scss index 172ac1f34d..3e6678a3cf 100644 --- a/src/dashboard/Data/Agent/Agent.scss +++ b/src/dashboard/Data/Agent/Agent.scss @@ -5,6 +5,7 @@ * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ +@import 'stylesheets/globals.scss'; .agentContainer { display: flex; @@ -44,6 +45,12 @@ pointer-events: auto; /* Re-enable pointer events for the actual content */ } +body:global(.expanded) { + .emptyStateOverlay { + left: $sidebarCollapsedWidth; + } +} + .messagesContainer { display: flex; flex-direction: column; From c4a25ded1178a730fe5037e0f3b13704162d11b3 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 08:08:10 +0200 Subject: [PATCH 03/42] Update Agent.scss --- src/dashboard/Data/Agent/Agent.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dashboard/Data/Agent/Agent.scss b/src/dashboard/Data/Agent/Agent.scss index 3e6678a3cf..44cd04db0c 100644 --- a/src/dashboard/Data/Agent/Agent.scss +++ b/src/dashboard/Data/Agent/Agent.scss @@ -25,6 +25,7 @@ flex: 1; overflow-y: auto; padding: 20px; + padding-top: 116px; /* Add top padding to account for the 96px fixed toolbar + some extra spacing */ background-color: #f8f9fa; } From 749d9ec68f3213c646e13f357e9432c4b5b75a26 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 08:13:16 +0200 Subject: [PATCH 04/42] menu --- src/dashboard/Data/Agent/Agent.react.js | 11 +++++------ src/dashboard/Data/Agent/Agent.scss | 15 --------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/src/dashboard/Data/Agent/Agent.react.js b/src/dashboard/Data/Agent/Agent.react.js index 4b07a148fe..f4b6f453da 100644 --- a/src/dashboard/Data/Agent/Agent.react.js +++ b/src/dashboard/Data/Agent/Agent.react.js @@ -5,8 +5,10 @@ * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ +import BrowserMenu from 'components/BrowserMenu/BrowserMenu.react'; import DashboardView from 'dashboard/DashboardView.react'; import EmptyState from 'components/EmptyState/EmptyState.react'; +import MenuItem from 'components/BrowserMenu/MenuItem.react'; import React from 'react'; import SidebarAction from 'components/Sidebar/SidebarAction'; import Toolbar from 'components/Toolbar/Toolbar.react'; @@ -78,12 +80,9 @@ class Agent extends DashboardView { renderToolbar() { return ( - + + this.clearChat()} /> + ); } diff --git a/src/dashboard/Data/Agent/Agent.scss b/src/dashboard/Data/Agent/Agent.scss index 44cd04db0c..cc2a46cf1d 100644 --- a/src/dashboard/Data/Agent/Agent.scss +++ b/src/dashboard/Data/Agent/Agent.scss @@ -185,18 +185,3 @@ body:global(.expanded) { background-color: #6c757d; cursor: not-allowed; } - -.clearButton { - padding: 8px 16px; - background-color: #dc3545; - color: white; - border: none; - border-radius: 4px; - font-size: 13px; - cursor: pointer; - transition: background-color 0.2s ease; -} - -.clearButton:hover { - background-color: #c82333; -} From 56759ccb100c5198fe5b0b1ba3ae01d5899558b2 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 08:17:51 +0200 Subject: [PATCH 05/42] Update Agent.react.js --- src/dashboard/Data/Agent/Agent.react.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/dashboard/Data/Agent/Agent.react.js b/src/dashboard/Data/Agent/Agent.react.js index f4b6f453da..d4d3ffa60f 100644 --- a/src/dashboard/Data/Agent/Agent.react.js +++ b/src/dashboard/Data/Agent/Agent.react.js @@ -28,11 +28,16 @@ class Agent extends DashboardView { isLoading: false, }; + this.browserMenuRef = React.createRef(); this.action = new SidebarAction('Clear Chat', () => this.clearChat()); } clearChat() { this.setState({ messages: [] }); + // Close the menu by simulating an external click + if (this.browserMenuRef.current) { + this.browserMenuRef.current.setState({ open: false }); + } } handleInputChange = (event) => { @@ -80,7 +85,12 @@ class Agent extends DashboardView { renderToolbar() { return ( - + {}} + > this.clearChat()} /> From 4db1650fd184c805e3b14a5788bafdf2a575ff47 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 08:22:52 +0200 Subject: [PATCH 06/42] Update Agent.react.js --- src/dashboard/Data/Agent/Agent.react.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/dashboard/Data/Agent/Agent.react.js b/src/dashboard/Data/Agent/Agent.react.js index d4d3ffa60f..28c1b8b5ba 100644 --- a/src/dashboard/Data/Agent/Agent.react.js +++ b/src/dashboard/Data/Agent/Agent.react.js @@ -29,6 +29,7 @@ class Agent extends DashboardView { }; this.browserMenuRef = React.createRef(); + this.chatInputRef = React.createRef(); this.action = new SidebarAction('Clear Chat', () => this.clearChat()); } @@ -79,6 +80,11 @@ class Agent extends DashboardView { messages: [...prevState.messages, aiMessage], isLoading: false, })); + + // Focus the input field after the AI response + if (this.chatInputRef.current) { + this.chatInputRef.current.focus(); + } }, 1000); } @@ -141,12 +147,14 @@ class Agent extends DashboardView {
From 2fead432337128540a2b60944ff6f99c88da342b Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:25:59 +0200 Subject: [PATCH 09/42] api --- Parse-Dashboard/app.js | 125 ++++++++++++++++++++++++ package-lock.json | 116 +++++++++++++++++++--- package.json | 1 + src/dashboard/Data/Agent/Agent.react.js | 75 ++++++++++++-- src/dashboard/Data/Agent/Agent.scss | 6 ++ src/lib/AgentService.js | 109 +++++++++++++++++++++ 6 files changed, 408 insertions(+), 24 deletions(-) create mode 100644 src/lib/AgentService.js diff --git a/Parse-Dashboard/app.js b/Parse-Dashboard/app.js index 24b49ded48..58676dbfec 100644 --- a/Parse-Dashboard/app.js +++ b/Parse-Dashboard/app.js @@ -182,6 +182,131 @@ module.exports = function(config, options) { res.send({ success: false, error: 'Something went wrong.' }); }); + // Agent API endpoint for handling AI requests + app.post('/agent', csrf(), async (req, res) => { + try { + const { message, modelName } = req.body; + + if (!message || typeof message !== 'string' || message.trim() === '') { + return res.status(400).json({ error: 'Message is required' }); + } + + if (!modelName || typeof modelName !== 'string') { + return res.status(400).json({ error: 'Model name is required' }); + } + + // Check if agent configuration exists + if (!config.agent || !config.agent.models || !Array.isArray(config.agent.models)) { + return res.status(400).json({ error: 'No agent configuration found' }); + } + + // Find the requested model + const modelConfig = config.agent.models.find(model => model.name === modelName); + if (!modelConfig) { + return res.status(400).json({ error: `Model "${modelName}" not found in configuration` }); + } + + // Validate model configuration + const { provider, model, apiKey } = modelConfig; + if (!provider || !model || !apiKey) { + return res.status(400).json({ error: 'Model configuration is incomplete' }); + } + + if (apiKey === 'xxxxx' || apiKey.includes('xxx')) { + return res.status(400).json({ error: 'Please replace the placeholder API key with your actual API key' }); + } + + // Only support OpenAI for now + if (provider.toLowerCase() !== 'openai') { + return res.status(400).json({ error: `Provider "${provider}" is not supported yet` }); + } + + // Make request to OpenAI API + const response = await makeOpenAIRequest(message, model, apiKey); + res.json({ response }); + + } catch (error) { + console.error('Agent API error:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } + }); + + /** + * Make a request to OpenAI API + */ + async function makeOpenAIRequest(message, model, apiKey) { + const fetch = (await import('node-fetch')).default; + + const url = 'https://api.openai.com/v1/chat/completions'; + + const messages = [ + { + role: 'system', + content: `You are an AI assistant integrated into Parse Dashboard, a data management interface for Parse Server applications. + +Your role is to help users with: +- Database queries and data operations +- Understanding Parse Server concepts +- Troubleshooting common issues +- Best practices for data modeling +- Cloud Code and server configuration + +When responding: +- Be concise and helpful +- Provide practical examples when relevant +- Ask clarifying questions if the user's request is unclear +- Focus on Parse-specific solutions and recommendations + +You have access to the user's Parse Dashboard interface, so you can reference their database schema, classes, and data when appropriate.` + }, + { + role: 'user', + content: message + } + ]; + + const requestBody = { + model: model, + messages: messages, + temperature: 0.7, + max_tokens: 1000, + stream: false + }; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + if (response.status === 401) { + throw new Error('Invalid API key. Please check your OpenAI API key configuration.'); + } else if (response.status === 429) { + throw new Error('Rate limit exceeded. Please try again in a moment.'); + } else if (response.status === 403) { + throw new Error('Access forbidden. Please check your API key permissions.'); + } else if (response.status >= 500) { + throw new Error('OpenAI service is temporarily unavailable. Please try again later.'); + } + + const errorData = await response.json().catch(() => ({})); + const errorMessage = errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`; + throw new Error(`OpenAI API error: ${errorMessage}`); + } + + const data = await response.json(); + + if (!data.choices || data.choices.length === 0) { + throw new Error('No response received from OpenAI API'); + } + + return data.choices[0].message.content; + } + // Serve the app icons. Uses the optional `iconsFolder` parameter as // directory name, that was setup in the config file. // We are explicitly not using `__dirpath` here because one may be diff --git a/package-lock.json b/package-lock.json index a4c0076ff9..753e3ab932 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "immutable-devtools": "0.1.5", "inquirer": "12.6.3", "js-beautify": "1.15.4", + "node-fetch": "3.3.2", "otpauth": "8.0.3", "package-json": "7.0.0", "parse": "3.5.1", @@ -9468,6 +9469,25 @@ "node-fetch": "2.6.7" } }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -11731,6 +11751,28 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fetch-node-website": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/fetch-node-website/-/fetch-node-website-9.0.1.tgz", @@ -12243,6 +12285,17 @@ "node": ">= 14.17" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -16207,6 +16260,25 @@ "license": "MIT", "optional": 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==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-emoji": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", @@ -16223,22 +16295,28 @@ } }, "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dependencies": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-fetch/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" } }, "node_modules/node-int64": { @@ -23541,7 +23619,7 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/traverse": { "version": "0.6.8", @@ -24289,10 +24367,18 @@ "defaults": "^1.0.3" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { "version": "5.99.9", @@ -24455,7 +24541,7 @@ "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/package.json b/package.json index ad10643454..d7a96c185d 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "immutable-devtools": "0.1.5", "inquirer": "12.6.3", "js-beautify": "1.15.4", + "node-fetch": "3.3.2", "otpauth": "8.0.3", "package-json": "7.0.0", "parse": "3.5.1", diff --git a/src/dashboard/Data/Agent/Agent.react.js b/src/dashboard/Data/Agent/Agent.react.js index c24e66069f..d39dba6b6b 100644 --- a/src/dashboard/Data/Agent/Agent.react.js +++ b/src/dashboard/Data/Agent/Agent.react.js @@ -13,6 +13,7 @@ import MenuItem from 'components/BrowserMenu/MenuItem.react'; import React from 'react'; import SidebarAction from 'components/Sidebar/SidebarAction'; import Toolbar from 'components/Toolbar/Toolbar.react'; +import AgentService from 'lib/AgentService'; import styles from './Agent.scss'; import { withRouter } from 'lib/withRouter'; @@ -37,7 +38,18 @@ class Agent extends DashboardView { getStoredSelectedModel() { const stored = localStorage.getItem('selectedAgentModel'); - return stored || null; + return stored; + } + + componentDidMount() { + // Set default selected model if none is selected and models are available + const { agentConfig } = this.props; + const { selectedModel } = this.state; + const models = agentConfig?.models || []; + + if (!selectedModel && models.length > 0) { + this.setSelectedModel(models[0].name); + } } setSelectedModel(modelName) { @@ -57,13 +69,34 @@ class Agent extends DashboardView { this.setState({ inputValue: event.target.value }); } - handleSubmit = (event) => { + handleSubmit = async (event) => { event.preventDefault(); - const { inputValue } = this.state; + const { inputValue, selectedModel } = this.state; + const { agentConfig } = this.props; if (inputValue.trim() === '') { return; } + + // Find the selected model configuration + const models = agentConfig?.models || []; + const modelConfig = models.find(model => model.name === selectedModel) || models[0]; + + if (!modelConfig) { + const errorMessage = { + id: Date.now() + 1, + type: 'agent', + content: 'No AI model is configured. Please check your dashboard configuration.', + timestamp: new Date(), + isError: true, + }; + + this.setState(prevState => ({ + messages: [...prevState.messages, errorMessage], + isLoading: false, + })); + return; + } // Add user message const userMessage = { @@ -79,12 +112,18 @@ class Agent extends DashboardView { isLoading: true, })); - // Simulate AI response (replace with actual AI integration later) - setTimeout(() => { + try { + // Validate model configuration + AgentService.validateModelConfig(modelConfig); + + // Get response from AI service + const instructions = AgentService.getDefaultInstructions(); + const response = await AgentService.sendMessage(inputValue.trim(), modelConfig, instructions); + const aiMessage = { id: Date.now() + 1, type: 'agent', - content: `I received your message: "${inputValue.trim()}". This is a placeholder response. AI integration will be implemented here.`, + content: response, timestamp: new Date(), }; @@ -93,11 +132,29 @@ class Agent extends DashboardView { isLoading: false, })); - // Focus the input field after the AI response + } catch (error) { + console.error('Agent API error:', error); + + const errorMessage = { + id: Date.now() + 1, + type: 'agent', + content: `Error: ${error.message}`, + timestamp: new Date(), + isError: true, + }; + + this.setState(prevState => ({ + messages: [...prevState.messages, errorMessage], + isLoading: false, + })); + } + + // Focus the input field after the response + setTimeout(() => { if (this.chatInputRef.current) { this.chatInputRef.current.focus(); } - }, 1000); + }, 100); } renderToolbar() { @@ -164,7 +221,7 @@ class Agent extends DashboardView { {messages.map((message) => (
{message.content} diff --git a/src/dashboard/Data/Agent/Agent.scss b/src/dashboard/Data/Agent/Agent.scss index cc2a46cf1d..7a7080c341 100644 --- a/src/dashboard/Data/Agent/Agent.scss +++ b/src/dashboard/Data/Agent/Agent.scss @@ -82,6 +82,12 @@ body:global(.expanded) { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } +.message.agent.error { + background-color: #f8d7da; + color: #721c24; + border-color: #f5c6cb; +} + .messageContent { font-size: 14px; line-height: 1.4; diff --git a/src/lib/AgentService.js b/src/lib/AgentService.js new file mode 100644 index 0000000000..4ac8286932 --- /dev/null +++ b/src/lib/AgentService.js @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2016-present, Parse, LLC + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ +import { post } from './AJAX'; + +/** + * Service class for handling AI agent API requests to different providers + */ +export default class AgentService { + /** + * Send a message to the configured AI model and get a response + * @param {string} message - The user's message + * @param {Object} modelConfig - The model configuration object + * @param {string|null} instructions - Optional system instructions for the AI (currently ignored, handled server-side) + * @returns {Promise} The AI's response + */ + static async sendMessage(message, modelConfig, instructions = null) { + if (!modelConfig) { + throw new Error('Model configuration is required'); + } + + const { name } = modelConfig; + + if (!name) { + throw new Error('Model name is required in model configuration'); + } + + try { + const response = await post('/agent', { + message: message, + modelName: name + }); + + if (response.error) { + throw new Error(response.error); + } + + return response.response; + } catch (error) { + // Handle network errors and other fetch-related errors + if (error.message && error.message.includes('fetch')) { + throw new Error('Network error: Unable to connect to agent service. Please check your internet connection.'); + } + throw error; + } + } + + /** + * Validate model configuration + * @param {Object} modelConfig - The model configuration object + * @returns {boolean} True if valid, throws error if invalid + */ + static validateModelConfig(modelConfig) { + if (!modelConfig) { + throw new Error('Model configuration is required'); + } + + const { name, provider, model, apiKey } = modelConfig; + + if (!name) { + throw new Error('Model name is required in model configuration'); + } + + if (!provider) { + throw new Error('Provider is required in model configuration'); + } + + if (!model) { + throw new Error('Model name is required in model configuration'); + } + + if (!apiKey) { + throw new Error('API key is required in model configuration'); + } + + if (apiKey === 'xxxxx' || apiKey.includes('xxx')) { + throw new Error('Please replace the placeholder API key with your actual API key'); + } + + return true; + } + + /** + * Get default system instructions for Parse Dashboard agent + * @returns {string} Default system instructions + */ + static getDefaultInstructions() { + return `You are an AI assistant integrated into Parse Dashboard, a data management interface for Parse Server applications. + +Your role is to help users with: +- Database queries and data operations +- Understanding Parse Server concepts +- Troubleshooting common issues +- Best practices for data modeling +- Cloud Code and server configuration + +When responding: +- Be concise and helpful +- Provide practical examples when relevant +- Ask clarifying questions if the user's request is unclear +- Focus on Parse-specific solutions and recommendations + +You have access to the user's Parse Dashboard interface, so you can reference their database schema, classes, and data when appropriate.`; + } +} From cd3b31c35bd2670b32c4f7a8b6bcd1de2e85d31f Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:00:51 +0200 Subject: [PATCH 10/42] fix routing --- src/dashboard/DashboardView.react.js | 1 + src/dashboard/Data/Agent/Agent.react.js | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/dashboard/DashboardView.react.js b/src/dashboard/DashboardView.react.js index e49751d54d..9f768060be 100644 --- a/src/dashboard/DashboardView.react.js +++ b/src/dashboard/DashboardView.react.js @@ -34,6 +34,7 @@ export default class DashboardView extends React.Component { onRouteChanged() { const path = this.props.location?.pathname ?? window.location.pathname; const route = path.split('apps')[1].split('/')[2]; + if (route !== this.state.route) { this.setState({ route }); } diff --git a/src/dashboard/Data/Agent/Agent.react.js b/src/dashboard/Data/Agent/Agent.react.js index d39dba6b6b..8cd41d1f01 100644 --- a/src/dashboard/Data/Agent/Agent.react.js +++ b/src/dashboard/Data/Agent/Agent.react.js @@ -42,6 +42,22 @@ class Agent extends DashboardView { } componentDidMount() { + // Fix the routing issue by ensuring this.state.route is set to 'agent' + if (this.state.route !== 'agent') { + this.setState({ route: 'agent' }); + } + + this.setDefaultModel(); + } + + componentDidUpdate(prevProps) { + // If agentConfig just became available, set default model + if (!prevProps.agentConfig && this.props.agentConfig) { + this.setDefaultModel(); + } + } + + setDefaultModel() { // Set default selected model if none is selected and models are available const { agentConfig } = this.props; const { selectedModel } = this.state; @@ -162,11 +178,6 @@ class Agent extends DashboardView { const { selectedModel } = this.state; const models = agentConfig?.models || []; - // Debug logging - console.log('Agent props:', this.props); - console.log('agentConfig:', agentConfig); - console.log('models:', models); - return ( {models.length > 0 && ( From b9a4e19f32332f315c400e975edb32749544b877 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:06:12 +0200 Subject: [PATCH 11/42] error handling --- Parse-Dashboard/app.js | 9 +++++++-- src/dashboard/Data/Agent/Agent.react.js | 11 ++++++++++- src/lib/AgentService.js | 11 +++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Parse-Dashboard/app.js b/Parse-Dashboard/app.js index 58676dbfec..5ce224bafb 100644 --- a/Parse-Dashboard/app.js +++ b/Parse-Dashboard/app.js @@ -84,8 +84,13 @@ module.exports = function(config, options) { if (err.code !== 'EBADCSRFTOKEN') {return next(err)} // handle CSRF token errors here - res.status(403) - res.send('form tampered with') + console.error('CSRF token validation failed for request:', req.url, req.method); + res.status(403); + if (req.xhr || req.headers.accept?.indexOf('json') > -1) { + res.json({ error: 'CSRF token validation failed. Please refresh the page and try again.' }); + } else { + res.send('CSRF token validation failed. Please refresh the page and try again.'); + } }); // Serve the configuration. diff --git a/src/dashboard/Data/Agent/Agent.react.js b/src/dashboard/Data/Agent/Agent.react.js index 8cd41d1f01..a28ecf84fc 100644 --- a/src/dashboard/Data/Agent/Agent.react.js +++ b/src/dashboard/Data/Agent/Agent.react.js @@ -151,10 +151,19 @@ class Agent extends DashboardView { } catch (error) { console.error('Agent API error:', error); + let errorContent = `Error: ${error.message}`; + + // Handle specific error types + if (error.message && error.message.includes('Permission Denied')) { + errorContent = 'Error: Permission denied. Please refresh the page and try again.'; + } else if (error.message && error.message.includes('CSRF')) { + errorContent = 'Error: Security token expired. Please refresh the page and try again.'; + } + const errorMessage = { id: Date.now() + 1, type: 'agent', - content: `Error: ${error.message}`, + content: errorContent, timestamp: new Date(), isError: true, }; diff --git a/src/lib/AgentService.js b/src/lib/AgentService.js index 4ac8286932..542bfa9b24 100644 --- a/src/lib/AgentService.js +++ b/src/lib/AgentService.js @@ -41,10 +41,21 @@ export default class AgentService { return response.response; } catch (error) { + // Handle specific error types + if (error.message && error.message.includes('Permission Denied')) { + throw new Error('Permission denied. Please refresh the page and try again.'); + } + + if (error.message && error.message.includes('CSRF')) { + throw new Error('Security token expired. Please refresh the page and try again.'); + } + // Handle network errors and other fetch-related errors if (error.message && error.message.includes('fetch')) { throw new Error('Network error: Unable to connect to agent service. Please check your internet connection.'); } + + // Re-throw the original error if it's not a recognized type throw error; } } From 56b740714c68b59bdc814d703810d64370f5a792 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:55:41 +0200 Subject: [PATCH 12/42] fix --- Parse-Dashboard/app.js | 39 +++++++++++++++++++------ src/dashboard/Data/Agent/Agent.react.js | 8 ++++- src/lib/AJAX.js | 22 +++++++++++--- src/lib/AgentService.js | 9 ++++-- 4 files changed, 62 insertions(+), 16 deletions(-) diff --git a/Parse-Dashboard/app.js b/Parse-Dashboard/app.js index 5ce224bafb..07cdb58671 100644 --- a/Parse-Dashboard/app.js +++ b/Parse-Dashboard/app.js @@ -63,6 +63,11 @@ function checkIfIconsExistForApps(apps, iconsFolder) { module.exports = function(config, options) { options = options || {}; const app = express(); + + // Parse JSON and URL-encoded request bodies + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + // Serve public files. app.use(express.static(path.join(__dirname,'public'))); @@ -84,7 +89,6 @@ module.exports = function(config, options) { if (err.code !== 'EBADCSRFTOKEN') {return next(err)} // handle CSRF token errors here - console.error('CSRF token validation failed for request:', req.url, req.method); res.status(403); if (req.xhr || req.headers.accept?.indexOf('json') > -1) { res.json({ error: 'CSRF token validation failed. Please refresh the page and try again.' }); @@ -187,10 +191,12 @@ module.exports = function(config, options) { res.send({ success: false, error: 'Something went wrong.' }); }); - // Agent API endpoint for handling AI requests - app.post('/agent', csrf(), async (req, res) => { + // Agent API endpoint for handling AI requests - scoped to specific app + // Temporarily disable CSRF for debugging + app.post('/apps/:appId/agent', async (req, res) => { try { const { message, modelName } = req.body; + const { appId } = req.params; if (!message || typeof message !== 'string' || message.trim() === '') { return res.status(400).json({ error: 'Message is required' }); @@ -200,11 +206,21 @@ module.exports = function(config, options) { return res.status(400).json({ error: 'Model name is required' }); } + if (!appId || typeof appId !== 'string') { + return res.status(400).json({ error: 'App ID is required' }); + } + // Check if agent configuration exists if (!config.agent || !config.agent.models || !Array.isArray(config.agent.models)) { return res.status(400).json({ error: 'No agent configuration found' }); } + // Find the app in the configuration + const app = config.apps.find(app => (app.appNameForURL || app.appName) === appId); + if (!app) { + return res.status(404).json({ error: `App "${appId}" not found` }); + } + // Find the requested model const modelConfig = config.agent.models.find(model => model.name === modelName); if (!modelConfig) { @@ -226,24 +242,29 @@ module.exports = function(config, options) { return res.status(400).json({ error: `Provider "${provider}" is not supported yet` }); } - // Make request to OpenAI API - const response = await makeOpenAIRequest(message, model, apiKey); + // Make request to OpenAI API with app context + const response = await makeOpenAIRequest(message, model, apiKey, app); res.json({ response }); } catch (error) { - console.error('Agent API error:', error); - res.status(500).json({ error: error.message || 'Internal server error' }); + // Return the full error message to help with debugging + const errorMessage = error.message || 'Provider error'; + res.status(500).json({ error: `Error: ${errorMessage}` }); } }); /** * Make a request to OpenAI API */ - async function makeOpenAIRequest(message, model, apiKey) { + async function makeOpenAIRequest(message, model, apiKey, appContext = null) { const fetch = (await import('node-fetch')).default; const url = 'https://api.openai.com/v1/chat/completions'; + const appInfo = appContext ? + `\n\nContext: You are currently helping with the Parse Server app "${appContext.appName}" (ID: ${appContext.appId}) at ${appContext.serverURL}.` : + ''; + const messages = [ { role: 'system', @@ -262,7 +283,7 @@ When responding: - Ask clarifying questions if the user's request is unclear - Focus on Parse-specific solutions and recommendations -You have access to the user's Parse Dashboard interface, so you can reference their database schema, classes, and data when appropriate.` +You have access to the user's Parse Dashboard interface, so you can reference their database schema, classes, and data when appropriate.${appInfo}` }, { role: 'user', diff --git a/src/dashboard/Data/Agent/Agent.react.js b/src/dashboard/Data/Agent/Agent.react.js index a28ecf84fc..758a1212ba 100644 --- a/src/dashboard/Data/Agent/Agent.react.js +++ b/src/dashboard/Data/Agent/Agent.react.js @@ -132,9 +132,15 @@ class Agent extends DashboardView { // Validate model configuration AgentService.validateModelConfig(modelConfig); + // Get app slug from context + const appSlug = this.context ? this.context.slug : null; + if (!appSlug) { + throw new Error('App context not available'); + } + // Get response from AI service const instructions = AgentService.getDefaultInstructions(); - const response = await AgentService.sendMessage(inputValue.trim(), modelConfig, instructions); + const response = await AgentService.sendMessage(inputValue.trim(), modelConfig, instructions, appSlug); const aiMessage = { id: Date.now() + 1, diff --git a/src/lib/AJAX.js b/src/lib/AJAX.js index 91b3d7506c..ea98d30a7b 100644 --- a/src/lib/AJAX.js +++ b/src/lib/AJAX.js @@ -98,12 +98,26 @@ export function request( notice: message, }); } else if (this.status >= 500) { + let json = {}; + try { + json = JSON.parse(this.responseText); + } catch { + p.reject({ + success: false, + message: 'Server Error', + error: 'Server Error', + errors: ['Server Error'], + notice: 'Server Error', + }); + return; + } + const message = json.message || json.error || json.notice || 'Server Error'; p.reject({ success: false, - message: 'Server Error', - error: 'Server Error', - errors: ['Server Error'], - notice: 'Server Error', + message: message, + error: message, + errors: json.errors || [message], + notice: message, }); } }; diff --git a/src/lib/AgentService.js b/src/lib/AgentService.js index 542bfa9b24..7bb562b941 100644 --- a/src/lib/AgentService.js +++ b/src/lib/AgentService.js @@ -16,9 +16,10 @@ export default class AgentService { * @param {string} message - The user's message * @param {Object} modelConfig - The model configuration object * @param {string|null} instructions - Optional system instructions for the AI (currently ignored, handled server-side) + * @param {string} appSlug - The app slug to scope the request to * @returns {Promise} The AI's response */ - static async sendMessage(message, modelConfig, instructions = null) { + static async sendMessage(message, modelConfig, instructions = null, appSlug) { if (!modelConfig) { throw new Error('Model configuration is required'); } @@ -29,8 +30,12 @@ export default class AgentService { throw new Error('Model name is required in model configuration'); } + if (!appSlug) { + throw new Error('App slug is required to send message to agent'); + } + try { - const response = await post('/agent', { + const response = await post(`/apps/${appSlug}/agent`, { message: message, modelName: name }); From 450a73f373e765b99bf2a9d7ae42d5829fdf883b Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:56:34 +0200 Subject: [PATCH 13/42] Update app.js --- Parse-Dashboard/app.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Parse-Dashboard/app.js b/Parse-Dashboard/app.js index 07cdb58671..b7c96edea4 100644 --- a/Parse-Dashboard/app.js +++ b/Parse-Dashboard/app.js @@ -192,7 +192,6 @@ module.exports = function(config, options) { }); // Agent API endpoint for handling AI requests - scoped to specific app - // Temporarily disable CSRF for debugging app.post('/apps/:appId/agent', async (req, res) => { try { const { message, modelName } = req.body; From d7849b1bab7f50914b05af4a1da7f3afd79aac20 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:26:43 +0200 Subject: [PATCH 14/42] parse ops --- Parse-Dashboard/app.js | 470 +++++++++++++++++++++++- src/dashboard/Data/Agent/Agent.react.js | 49 ++- src/dashboard/Data/Agent/Agent.scss | 47 +++ src/lib/AgentService.js | 37 +- 4 files changed, 584 insertions(+), 19 deletions(-) diff --git a/Parse-Dashboard/app.js b/Parse-Dashboard/app.js index b7c96edea4..1295abfd89 100644 --- a/Parse-Dashboard/app.js +++ b/Parse-Dashboard/app.js @@ -252,10 +252,339 @@ module.exports = function(config, options) { } }); + /** + * Database function tools for the AI agent + */ + const databaseTools = [ + { + type: "function", + function: { + name: "queryClass", + description: "Query a Parse class/table to retrieve objects. Use this to fetch data from the database.", + parameters: { + type: "object", + properties: { + className: { + type: "string", + description: "The name of the Parse class to query" + }, + where: { + type: "object", + description: "Query constraints as a JSON object (e.g., {\"name\": \"John\", \"age\": {\"$gte\": 18}})" + }, + limit: { + type: "number", + description: "Maximum number of results to return (default 100, max 1000)" + }, + skip: { + type: "number", + description: "Number of results to skip for pagination" + }, + order: { + type: "string", + description: "Field to order by (prefix with '-' for descending, e.g., '-createdAt')" + }, + include: { + type: "array", + items: { type: "string" }, + description: "Array of pointer fields to include/populate" + }, + select: { + type: "array", + items: { type: "string" }, + description: "Array of fields to select (if not provided, all fields are returned)" + } + }, + required: ["className"] + } + } + }, + { + type: 'function', + function: { + name: 'createObject', + description: 'Create a new object in a Parse class/table. IMPORTANT: This is a write operation that requires explicit user confirmation before execution. You must ask the user to confirm before calling this function.', + parameters: { + type: 'object', + properties: { + className: { + type: 'string', + description: 'The name of the Parse class to create an object in' + }, + data: { + type: 'object', + description: 'The data for the new object as a JSON object' + }, + confirmed: { + type: 'boolean', + description: 'Must be true to indicate user has explicitly confirmed this write operation', + default: false + } + }, + required: ['className', 'data', 'confirmed'] + } + } + }, + { + type: 'function', + function: { + name: 'updateObject', + description: 'Update an existing object in a Parse class/table. IMPORTANT: This is a write operation that requires explicit user confirmation before execution. You must ask the user to confirm before calling this function.', + parameters: { + type: 'object', + properties: { + className: { + type: 'string', + description: 'The name of the Parse class containing the object' + }, + objectId: { + type: 'string', + description: 'The objectId of the object to update' + }, + data: { + type: 'object', + description: 'The fields to update as a JSON object' + }, + confirmed: { + type: 'boolean', + description: 'Must be true to indicate user has explicitly confirmed this write operation', + default: false + } + }, + required: ['className', 'objectId', 'data', 'confirmed'] + } + } + }, + { + type: 'function', + function: { + name: 'deleteObject', + description: 'Delete an object from a Parse class/table. IMPORTANT: This is a destructive write operation that requires explicit user confirmation before execution. You must ask the user to confirm before calling this function.', + parameters: { + type: 'object', + properties: { + className: { + type: 'string', + description: 'The name of the Parse class containing the object' + }, + objectId: { + type: 'string', + description: 'The objectId of the object to delete' + }, + confirmed: { + type: 'boolean', + description: 'Must be true to indicate user has explicitly confirmed this destructive operation', + default: false + } + }, + required: ['className', 'objectId', 'confirmed'] + } + } + }, + { + type: "function", + function: { + name: "getSchema", + description: "Get the schema information for Parse classes. Use this to understand the structure of classes/tables.", + parameters: { + type: "object", + properties: { + className: { + type: "string", + description: "The name of the Parse class to get schema for (optional - if not provided, returns all schemas)" + } + } + } + } + }, + { + type: "function", + function: { + name: "countObjects", + description: "Count objects in a Parse class/table that match given constraints.", + parameters: { + type: "object", + properties: { + className: { + type: "string", + description: "The name of the Parse class to count objects in" + }, + where: { + type: "object", + description: "Query constraints as a JSON object (optional)" + } + }, + required: ["className"] + } + } + } + ]; + + /** + * Execute database function calls + */ + async function executeDatabaseFunction(functionName, args, appContext) { + const Parse = require('parse/node'); + + // Initialize Parse for this app context + Parse.initialize(appContext.appId, undefined, appContext.masterKey); + Parse.serverURL = appContext.serverURL; + Parse.masterKey = appContext.masterKey; + + try { + switch (functionName) { + case 'queryClass': { + const { className, where = {}, limit = 100, skip = 0, order, include = [], select = [] } = args; + const query = new Parse.Query(className); + + // Apply constraints + Object.keys(where).forEach(key => { + const value = where[key]; + if (typeof value === 'object' && value !== null) { + // Handle complex queries like {$gte: 18} + Object.keys(value).forEach(op => { + switch (op) { + case '$gt': query.greaterThan(key, value[op]); break; + case '$gte': query.greaterThanOrEqualTo(key, value[op]); break; + case '$lt': query.lessThan(key, value[op]); break; + case '$lte': query.lessThanOrEqualTo(key, value[op]); break; + case '$ne': query.notEqualTo(key, value[op]); break; + case '$in': query.containedIn(key, value[op]); break; + case '$nin': query.notContainedIn(key, value[op]); break; + case '$exists': + if (value[op]) query.exists(key); + else query.doesNotExist(key); + break; + case '$regex': query.matches(key, new RegExp(value[op], value.$options || '')); break; + } + }); + } else { + query.equalTo(key, value); + } + }); + + if (limit) query.limit(Math.min(limit, 1000)); + if (skip) query.skip(skip); + if (order) { + if (order.startsWith('-')) { + query.descending(order.substring(1)); + } else { + query.ascending(order); + } + } + if (include.length > 0) query.include(include); + if (select.length > 0) query.select(select); + + const results = await query.find({ useMasterKey: true }); + return results.map(obj => obj.toJSON()); + } + + case 'createObject': { + const { className, data, confirmed } = args; + + // Require explicit confirmation for write operations + if (!confirmed) { + throw new Error('CONFIRMATION_REQUIRED: Creating objects requires user confirmation. Please confirm that you want to create a new object in the ' + className + ' class with this data.'); + } + + const ParseObject = Parse.Object.extend(className); + const object = new ParseObject(); + + Object.keys(data).forEach(key => { + object.set(key, data[key]); + }); + + const result = await object.save(null, { useMasterKey: true }); + return result.toJSON(); + } + + case 'updateObject': { + const { className, objectId, data, confirmed } = args; + + // Require explicit confirmation for write operations + if (!confirmed) { + throw new Error('CONFIRMATION_REQUIRED: Updating objects requires user confirmation. Please confirm that you want to update the object ' + objectId + ' in the ' + className + ' class.'); + } + + const query = new Parse.Query(className); + const object = await query.get(objectId, { useMasterKey: true }); + + Object.keys(data).forEach(key => { + object.set(key, data[key]); + }); + + const result = await object.save(null, { useMasterKey: true }); + return result.toJSON(); + } + + case 'deleteObject': { + const { className, objectId, confirmed } = args; + + // Require explicit confirmation for destructive operations + if (!confirmed) { + throw new Error('CONFIRMATION_REQUIRED: Deleting objects is a destructive operation that requires user confirmation. Please confirm that you want to permanently delete the object ' + objectId + ' from the ' + className + ' class. This action cannot be undone.'); + } + + const query = new Parse.Query(className); + const object = await query.get(objectId, { useMasterKey: true }); + await object.destroy({ useMasterKey: true }); + return { success: true, objectId }; + } + + case 'getSchema': { + const { className } = args; + if (className) { + const schema = await new Parse.Schema(className).get(); + return schema; + } else { + const schemas = await Parse.Schema.all(); + return schemas; + } + } + + case 'countObjects': { + const { className, where = {} } = args; + const query = new Parse.Query(className); + + Object.keys(where).forEach(key => { + const value = where[key]; + if (typeof value === 'object' && value !== null) { + Object.keys(value).forEach(op => { + switch (op) { + case '$gt': query.greaterThan(key, value[op]); break; + case '$gte': query.greaterThanOrEqualTo(key, value[op]); break; + case '$lt': query.lessThan(key, value[op]); break; + case '$lte': query.lessThanOrEqualTo(key, value[op]); break; + case '$ne': query.notEqualTo(key, value[op]); break; + case '$in': query.containedIn(key, value[op]); break; + case '$nin': query.notContainedIn(key, value[op]); break; + case '$exists': + if (value[op]) query.exists(key); + else query.doesNotExist(key); + break; + } + }); + } else { + query.equalTo(key, value); + } + }); + + const count = await query.count({ useMasterKey: true }); + return { count }; + } + + default: + throw new Error(`Unknown function: ${functionName}`); + } + } catch (error) { + throw new Error(`Database operation failed: ${error.message}`); + } + } + /** * Make a request to OpenAI API */ - async function makeOpenAIRequest(message, model, apiKey, appContext = null) { + async function makeOpenAIRequest(userMessage, model, apiKey, appContext = null) { const fetch = (await import('node-fetch')).default; const url = 'https://api.openai.com/v1/chat/completions'; @@ -270,23 +599,52 @@ module.exports = function(config, options) { content: `You are an AI assistant integrated into Parse Dashboard, a data management interface for Parse Server applications. Your role is to help users with: -- Database queries and data operations -- Understanding Parse Server concepts +- Database queries and data operations using the Parse JS SDK +- Understanding Parse Server concepts and best practices - Troubleshooting common issues - Best practices for data modeling -- Cloud Code and server configuration +- Cloud Code and server configuration guidance + +You have access to database function tools that allow you to: +- Query classes/tables to retrieve objects (read-only, no confirmation needed) +- Create new objects in classes (REQUIRES USER CONFIRMATION) +- Update existing objects (REQUIRES USER CONFIRMATION) +- Delete objects (REQUIRES USER CONFIRMATION) +- Get schema information for classes (read-only, no confirmation needed) +- Count objects that match certain criteria (read-only, no confirmation needed) + +CRITICAL SECURITY RULE FOR WRITE OPERATIONS: +- ANY write operation (create, update, delete) MUST have explicit user confirmation BEFORE execution +- You MUST ask the user to confirm each write operation individually +- You CANNOT assume consent or perform write operations without explicit permission +- The user cannot disable this confirmation requirement +- Even if the user says "yes to all" or similar, you must still ask for each operation +- If a user requests multiple write operations, ask for confirmation for each one separately + +When working with the database: +- Read operations (query, getSchema, count) can be performed immediately +- Write operations require the pattern: 1) Explain what you'll do, 2) Ask for confirmation, 3) Only then execute if confirmed +- Always use the provided database functions instead of writing code +- Class names are case-sensitive +- Use proper Parse query syntax for complex queries +- Handle objectId fields correctly +- Be mindful of data types (Date, Pointer, etc.) +- Always consider security and use appropriate query constraints +- Provide clear explanations of what database operations you're performing When responding: - Be concise and helpful - Provide practical examples when relevant - Ask clarifying questions if the user's request is unclear - Focus on Parse-specific solutions and recommendations +- If you perform database operations, explain what you did and show the results +- For write operations, always explain the impact and ask for explicit confirmation -You have access to the user's Parse Dashboard interface, so you can reference their database schema, classes, and data when appropriate.${appInfo}` +You have direct access to the Parse database through function calls, so you can query actual data and provide real-time information.${appInfo}` }, { role: 'user', - content: message + content: userMessage } ]; @@ -294,7 +652,9 @@ You have access to the user's Parse Dashboard interface, so you can reference th model: model, messages: messages, temperature: 0.7, - max_tokens: 1000, + max_tokens: 2000, + tools: databaseTools, + tool_choice: 'auto', stream: false }; @@ -319,17 +679,107 @@ You have access to the user's Parse Dashboard interface, so you can reference th } const errorData = await response.json().catch(() => ({})); - const errorMessage = errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`; + const errorMessage = (errorData && typeof errorData === 'object' && 'error' in errorData && errorData.error && typeof errorData.error === 'object' && 'message' in errorData.error) + ? errorData.error.message + : `HTTP ${response.status}: ${response.statusText}`; throw new Error(`OpenAI API error: ${errorMessage}`); } const data = await response.json(); - if (!data.choices || data.choices.length === 0) { + if (!data || typeof data !== 'object' || !('choices' in data) || !Array.isArray(data.choices) || data.choices.length === 0) { throw new Error('No response received from OpenAI API'); } - return data.choices[0].message.content; + const choice = data.choices[0]; + const responseMessage = choice.message; + + // Handle function calls + if (responseMessage.tool_calls && responseMessage.tool_calls.length > 0) { + const toolCalls = responseMessage.tool_calls; + const toolResponses = []; + + for (const toolCall of toolCalls) { + if (toolCall.type === 'function') { + try { + const functionName = toolCall.function.name; + const functionArgs = JSON.parse(toolCall.function.arguments); + + // Execute the database function + const result = await executeDatabaseFunction(functionName, functionArgs, appContext); + + toolResponses.push({ + tool_call_id: toolCall.id, + role: 'tool', + content: JSON.stringify(result) + }); + } catch (error) { + const functionName = toolCall.function.name; + const functionArgs = JSON.parse(toolCall.function.arguments); + + // Check if this is a confirmation error - these should be handled specially + if (error.message.startsWith('CONFIRMATION_REQUIRED:')) { + toolResponses.push({ + tool_call_id: toolCall.id, + role: 'tool', + content: JSON.stringify({ + error: error.message, + requiresConfirmation: true, + operation: functionName, + args: functionArgs + }) + }); + } else { + toolResponses.push({ + tool_call_id: toolCall.id, + role: 'tool', + content: JSON.stringify({ error: error.message }) + }); + } + } + } + } + + // Make a second request with the tool responses + const followUpMessages = [ + ...messages, + responseMessage, + ...toolResponses + ]; + + const followUpRequestBody = { + model: model, + messages: followUpMessages, + temperature: 0.7, + max_tokens: 2000, + tools: databaseTools, + tool_choice: 'auto', + stream: false + }; + + const followUpResponse = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify(followUpRequestBody) + }); + + if (!followUpResponse.ok) { + throw new Error(`Follow-up request failed: ${followUpResponse.statusText}`); + } + + const followUpData = await followUpResponse.json(); + + if (!followUpData || typeof followUpData !== 'object' || !('choices' in followUpData) || !Array.isArray(followUpData.choices) || followUpData.choices.length === 0) { + throw new Error('No follow-up response received from OpenAI API'); + } + + return followUpData.choices[0].message.content; + } + + return responseMessage.content; } // Serve the app icons. Uses the optional `iconsFolder` parameter as diff --git a/src/dashboard/Data/Agent/Agent.react.js b/src/dashboard/Data/Agent/Agent.react.js index 758a1212ba..e0c37f2b9f 100644 --- a/src/dashboard/Data/Agent/Agent.react.js +++ b/src/dashboard/Data/Agent/Agent.react.js @@ -85,6 +85,14 @@ class Agent extends DashboardView { this.setState({ inputValue: event.target.value }); } + handleExampleClick = (exampleText) => { + this.setState({ inputValue: exampleText }, () => { + // Auto-submit the example query + const event = { preventDefault: () => {} }; + this.handleSubmit(event); + }); + } + handleSubmit = async (event) => { event.preventDefault(); const { inputValue, selectedModel } = this.state; @@ -331,11 +339,42 @@ class Agent extends DashboardView { } /> ) : ( - +
+ +
+

Try asking:

+
+ + + + +
+
+
)}
)} diff --git a/src/dashboard/Data/Agent/Agent.scss b/src/dashboard/Data/Agent/Agent.scss index 7a7080c341..1613aa5dfd 100644 --- a/src/dashboard/Data/Agent/Agent.scss +++ b/src/dashboard/Data/Agent/Agent.scss @@ -191,3 +191,50 @@ body:global(.expanded) { background-color: #6c757d; cursor: not-allowed; } + +.emptyStateContainer { + text-align: center; + max-width: 600px; +} + +.exampleQueries { + margin-top: 24px; + + h4 { + color: #6c757d; + font-size: 14px; + font-weight: 500; + margin-bottom: 16px; + } +} + +.queryExamples { + display: flex; + flex-direction: column; + gap: 8px; + align-items: center; +} + +.exampleButton { + background: white; + border: 1px solid #007bff; + color: #007bff; + padding: 10px 16px; + border-radius: 20px; + font-size: 13px; + cursor: pointer; + transition: all 0.2s ease; + max-width: 280px; + text-align: center; + + &:hover { + background-color: #007bff; + color: white; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3); + } + + &:active { + transform: translateY(0); + } +} diff --git a/src/lib/AgentService.js b/src/lib/AgentService.js index 7bb562b941..f0166f3a37 100644 --- a/src/lib/AgentService.js +++ b/src/lib/AgentService.js @@ -108,18 +108,47 @@ export default class AgentService { return `You are an AI assistant integrated into Parse Dashboard, a data management interface for Parse Server applications. Your role is to help users with: -- Database queries and data operations -- Understanding Parse Server concepts +- Database queries and data operations using the Parse JS SDK +- Understanding Parse Server concepts and best practices - Troubleshooting common issues - Best practices for data modeling -- Cloud Code and server configuration +- Cloud Code and server configuration guidance + +You have access to database function tools that allow you to: +- Query classes/tables to retrieve objects (read-only, no confirmation needed) +- Create new objects in classes (REQUIRES USER CONFIRMATION) +- Update existing objects (REQUIRES USER CONFIRMATION) +- Delete objects (REQUIRES USER CONFIRMATION) +- Get schema information for classes (read-only, no confirmation needed) +- Count objects that match certain criteria (read-only, no confirmation needed) + +CRITICAL SECURITY RULE FOR WRITE OPERATIONS: +- ANY write operation (create, update, delete) MUST have explicit user confirmation BEFORE execution +- You MUST ask the user to confirm each write operation individually +- You CANNOT assume consent or perform write operations without explicit permission +- The user cannot disable this confirmation requirement +- Even if the user says "yes to all" or similar, you must still ask for each operation +- If a user requests multiple write operations, ask for confirmation for each one separately + +When working with the database: +- Read operations (query, getSchema, count) can be performed immediately +- Write operations require the pattern: 1) Explain what you'll do, 2) Ask for confirmation, 3) Only then execute if confirmed +- Always use the provided database functions to interact with data +- Class names are case-sensitive +- Use proper Parse query syntax for complex queries +- Handle objectId fields correctly +- Be mindful of data types (Date, Pointer, etc.) +- Always consider security and use appropriate query constraints +- Provide clear explanations of what database operations you're performing When responding: - Be concise and helpful - Provide practical examples when relevant - Ask clarifying questions if the user's request is unclear - Focus on Parse-specific solutions and recommendations +- If you perform database operations, explain what you did and show the results +- For write operations, always explain the impact and ask for explicit confirmation -You have access to the user's Parse Dashboard interface, so you can reference their database schema, classes, and data when appropriate.`; +You have direct access to the Parse database through function calls, so you can query actual data and provide real-time information about the user's Parse Dashboard interface.`; } } From e3e75ec27cd0ab89622758612f3523abe7d82733 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:33:51 +0200 Subject: [PATCH 15/42] context --- Parse-Dashboard/app.js | 55 +++++++++++++++++++++---- src/dashboard/Data/Agent/Agent.react.js | 19 +++++++-- src/lib/AgentService.js | 21 +++++++--- 3 files changed, 77 insertions(+), 18 deletions(-) diff --git a/Parse-Dashboard/app.js b/Parse-Dashboard/app.js index 1295abfd89..7cf93a8dc6 100644 --- a/Parse-Dashboard/app.js +++ b/Parse-Dashboard/app.js @@ -191,10 +191,13 @@ module.exports = function(config, options) { res.send({ success: false, error: 'Something went wrong.' }); }); + // In-memory conversation storage (consider using Redis in future) + const conversations = new Map(); + // Agent API endpoint for handling AI requests - scoped to specific app app.post('/apps/:appId/agent', async (req, res) => { try { - const { message, modelName } = req.body; + const { message, modelName, conversationId } = req.body; const { appId } = req.params; if (!message || typeof message !== 'string' || message.trim() === '') { @@ -241,9 +244,35 @@ module.exports = function(config, options) { return res.status(400).json({ error: `Provider "${provider}" is not supported yet` }); } - // Make request to OpenAI API with app context - const response = await makeOpenAIRequest(message, model, apiKey, app); - res.json({ response }); + // Get or create conversation history + const conversationKey = `${appId}_${conversationId || 'default'}`; + if (!conversations.has(conversationKey)) { + conversations.set(conversationKey, []); + } + + const conversationHistory = conversations.get(conversationKey); + + // Make request to OpenAI API with app context and conversation history + const response = await makeOpenAIRequest(message, model, apiKey, app, conversationHistory); + + // Update conversation history with user message and AI response + conversationHistory.push( + { role: 'user', content: message }, + { role: 'assistant', content: response } + ); + + // Keep conversation history to a reasonable size (last 20 messages) + if (conversationHistory.length > 20) { + conversationHistory.splice(0, conversationHistory.length - 20); + } + + // Generate or use provided conversation ID + const finalConversationId = conversationId || `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + res.json({ + response, + conversationId: finalConversationId + }); } catch (error) { // Return the full error message to help with debugging @@ -584,7 +613,7 @@ module.exports = function(config, options) { /** * Make a request to OpenAI API */ - async function makeOpenAIRequest(userMessage, model, apiKey, appContext = null) { + async function makeOpenAIRequest(userMessage, model, apiKey, appContext = null, conversationHistory = []) { const fetch = (await import('node-fetch')).default; const url = 'https://api.openai.com/v1/chat/completions'; @@ -593,6 +622,7 @@ module.exports = function(config, options) { `\n\nContext: You are currently helping with the Parse Server app "${appContext.appName}" (ID: ${appContext.appId}) at ${appContext.serverURL}.` : ''; + // Build messages array starting with system message const messages = [ { role: 'system', @@ -641,12 +671,19 @@ When responding: - For write operations, always explain the impact and ask for explicit confirmation You have direct access to the Parse database through function calls, so you can query actual data and provide real-time information.${appInfo}` - }, - { - role: 'user', - content: userMessage } ]; + + // Add conversation history if it exists + if (conversationHistory && conversationHistory.length > 0) { + messages.push(...conversationHistory); + } + + // Add the current user message + messages.push({ + role: 'user', + content: userMessage + }); const requestBody = { model: model, diff --git a/src/dashboard/Data/Agent/Agent.react.js b/src/dashboard/Data/Agent/Agent.react.js index e0c37f2b9f..06b6d88346 100644 --- a/src/dashboard/Data/Agent/Agent.react.js +++ b/src/dashboard/Data/Agent/Agent.react.js @@ -29,6 +29,7 @@ class Agent extends DashboardView { inputValue: '', isLoading: false, selectedModel: this.getStoredSelectedModel(), + conversationId: null, // Add conversation tracking }; this.browserMenuRef = React.createRef(); @@ -74,7 +75,10 @@ class Agent extends DashboardView { } clearChat() { - this.setState({ messages: [] }); + this.setState({ + messages: [], + conversationId: null // Reset conversation to start fresh + }); // Close the menu by simulating an external click if (this.browserMenuRef.current) { this.browserMenuRef.current.setState({ open: false }); @@ -146,20 +150,27 @@ class Agent extends DashboardView { throw new Error('App context not available'); } - // Get response from AI service + // Get response from AI service with conversation context const instructions = AgentService.getDefaultInstructions(); - const response = await AgentService.sendMessage(inputValue.trim(), modelConfig, instructions, appSlug); + const result = await AgentService.sendMessage( + inputValue.trim(), + modelConfig, + instructions, + appSlug, + this.state.conversationId + ); const aiMessage = { id: Date.now() + 1, type: 'agent', - content: response, + content: result.response, timestamp: new Date(), }; this.setState(prevState => ({ messages: [...prevState.messages, aiMessage], isLoading: false, + conversationId: result.conversationId, // Update conversation ID })); } catch (error) { diff --git a/src/lib/AgentService.js b/src/lib/AgentService.js index f0166f3a37..4a25761790 100644 --- a/src/lib/AgentService.js +++ b/src/lib/AgentService.js @@ -17,9 +17,10 @@ export default class AgentService { * @param {Object} modelConfig - The model configuration object * @param {string|null} instructions - Optional system instructions for the AI (currently ignored, handled server-side) * @param {string} appSlug - The app slug to scope the request to - * @returns {Promise} The AI's response + * @param {string|null} conversationId - Optional conversation ID to maintain context + * @returns {Promise<{response: string, conversationId: string}>} The AI's response and conversation ID */ - static async sendMessage(message, modelConfig, instructions = null, appSlug) { + static async sendMessage(message, modelConfig, instructions = null, appSlug, conversationId = null) { if (!modelConfig) { throw new Error('Model configuration is required'); } @@ -35,16 +36,26 @@ export default class AgentService { } try { - const response = await post(`/apps/${appSlug}/agent`, { + const requestBody = { message: message, modelName: name - }); + }; + + // Include conversation ID if provided + if (conversationId) { + requestBody.conversationId = conversationId; + } + + const response = await post(`/apps/${appSlug}/agent`, requestBody); if (response.error) { throw new Error(response.error); } - return response.response; + return { + response: response.response, + conversationId: response.conversationId + }; } catch (error) { // Handle specific error types if (error.message && error.message.includes('Permission Denied')) { From bc529e0ca9a3a26346c3ee311c65b7cb288ec265 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:54:09 +0200 Subject: [PATCH 16/42] empty layout fix --- src/components/EmptyState/EmptyState.react.js | 33 ++++++---- src/components/EmptyState/EmptyState.scss | 21 ++++++ src/dashboard/Data/Agent/Agent.react.js | 65 +++++++++---------- src/dashboard/Data/Agent/Agent.scss | 15 ++++- 4 files changed, 85 insertions(+), 49 deletions(-) diff --git a/src/components/EmptyState/EmptyState.react.js b/src/components/EmptyState/EmptyState.react.js index e791f82867..c9575bd074 100644 --- a/src/components/EmptyState/EmptyState.react.js +++ b/src/components/EmptyState/EmptyState.react.js @@ -41,18 +41,27 @@ const EmptyState = ({ action = () => {}, secondaryCta = '', secondaryAction = () => {}, -}) => ( -
-
- + customContent = null, + useFlexLayout = false, +}) => { + const containerClass = useFlexLayout ? styles.flexContainer : baseStyles.center; + + return ( +
+
+
+ +
+
{title}
+
{description}
+ {ctaButton(cta, action)} + {secondaryCta && ' '} + {ctaButton(secondaryCta, secondaryAction)} +
+ {customContent &&
{customContent}
}
-
{title}
-
{description}
- {ctaButton(cta, action)} - {secondaryCta && ' '} - {ctaButton(secondaryCta, secondaryAction)} -
-); + ); +}; EmptyState.propTypes = { icon: PropTypes.string.describe('The name of the large icon that appears in the empty state.'), @@ -72,6 +81,8 @@ EmptyState.propTypes = { secondaryAction: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).describe( 'An href link or a click handler that is forwarded to the secondary CTA button.' ), + customContent: PropTypes.node.describe('Custom content to render below the empty state.'), + useFlexLayout: PropTypes.bool.describe('Whether to use flex layout instead of absolute positioning.'), }; export default EmptyState; diff --git a/src/components/EmptyState/EmptyState.scss b/src/components/EmptyState/EmptyState.scss index 8fddab4268..122a52213c 100644 --- a/src/components/EmptyState/EmptyState.scss +++ b/src/components/EmptyState/EmptyState.scss @@ -7,6 +7,27 @@ */ @import 'stylesheets/globals.scss'; +.flexContainer { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 32px; +} + +.content { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + max-width: 600px; +} + +.customContent { + width: 100%; + max-width: 600px; +} + .title { font-size: 46px; font-weight: 100; diff --git a/src/dashboard/Data/Agent/Agent.react.js b/src/dashboard/Data/Agent/Agent.react.js index 06b6d88346..206e3df40b 100644 --- a/src/dashboard/Data/Agent/Agent.react.js +++ b/src/dashboard/Data/Agent/Agent.react.js @@ -350,42 +350,37 @@ class Agent extends DashboardView { } /> ) : ( -
- -
-

Try asking:

-
- - - - + +

Try asking:

+
+ + + +
-
-
+ } + /> )}
)} diff --git a/src/dashboard/Data/Agent/Agent.scss b/src/dashboard/Data/Agent/Agent.scss index 1613aa5dfd..d889226708 100644 --- a/src/dashboard/Data/Agent/Agent.scss +++ b/src/dashboard/Data/Agent/Agent.scss @@ -32,14 +32,17 @@ .emptyStateOverlay { position: fixed; left: 300px; - top: 0; + top: 116px; /* Account for toolbar height */ bottom: 0; right: 0; display: flex; + flex-direction: column; align-items: center; - justify-content: center; + justify-content: flex-start; + padding-top: 40px; /* Reduced padding */ pointer-events: none; /* Allow clicks to pass through to the input below */ z-index: 10; + overflow-y: auto; /* Allow scrolling if content is too tall */ } .emptyStateOverlay > * { @@ -195,10 +198,16 @@ body:global(.expanded) { .emptyStateContainer { text-align: center; max-width: 600px; + display: flex; + flex-direction: column; + align-items: center; + gap: 32px; /* Increased gap for better spacing */ + width: 100%; } .exampleQueries { - margin-top: 24px; + margin-top: 0; /* Remove margin since we're using gap in parent */ + width: 100%; h4 { color: #6c757d; From de116f61f3eb82c1f73dac82a828356ebd003717 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:56:54 +0200 Subject: [PATCH 17/42] example queries --- src/dashboard/Data/Agent/Agent.react.js | 4 ++-- src/dashboard/Data/Agent/Agent.scss | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dashboard/Data/Agent/Agent.react.js b/src/dashboard/Data/Agent/Agent.react.js index 206e3df40b..72a6b14b95 100644 --- a/src/dashboard/Data/Agent/Agent.react.js +++ b/src/dashboard/Data/Agent/Agent.react.js @@ -361,9 +361,9 @@ class Agent extends DashboardView {