diff --git a/functions/api/generateCollabDiagram.js b/functions/api/generateCollabDiagram.js new file mode 100644 index 000000000..5df0841cf --- /dev/null +++ b/functions/api/generateCollabDiagram.js @@ -0,0 +1,52 @@ +const { db } = require("../admin"); +const { openai } = require("../helpers/openai"); + +const extractJSON = text => { + try { + const start = text.indexOf("{"); + const end = text.lastIndexOf("}"); + if (end === -1 || start === -1) { + return { jsonObject: {}, isJSON: false }; + } + const jsonArrayString = text.slice(start, end + 1); + return { jsonObject: JSON.parse(jsonArrayString), isJSON: true }; + } catch (error) { + return { jsonObject: {}, isJSON: false }; + } +}; + +module.exports = async (req, res) => { + try { + const { documentDetailed } = req.body; + const promptDoc = await db.collection("diagramPrompts").doc("generate").get(); + const promptData = promptDoc.data(); + const llmPrompt = promptData.prompt; + console.log("llmPrompt", llmPrompt); + const prompt = ` + ${llmPrompt} + ${documentDetailed}`; + + const messages = [ + { + role: "user", + content: prompt + } + ]; + const model = "o1"; + const completion = await openai.chat.completions.create({ + messages, + model + }); + const response = extractJSON(completion.choices[0].message.content || "").jsonObject; + console.log(response); + if (!response?.groupHierarchy || !response?.nodes || !response?.links) { + throw Error("Incomplete JSON"); + } + return res.status(200).json({ response }); + } catch (e) { + console.error(e); + return res.status(500).json({ + message: e.message + }); + } +}; diff --git a/functions/api/improveCollabDiagram.js b/functions/api/improveCollabDiagram.js new file mode 100644 index 000000000..2d8a58633 --- /dev/null +++ b/functions/api/improveCollabDiagram.js @@ -0,0 +1,103 @@ +const { db } = require("../admin"); +/* const { openai } = require("../helpers/openai"); */ + +const extractJSON = text => { + try { + const start = text.indexOf("{"); + const end = text.lastIndexOf("}"); + if (end === -1 || start === -1) { + return { jsonObject: {}, isJSON: false }; + } + const jsonArrayString = text.slice(start, end + 1); + return { jsonObject: JSON.parse(jsonArrayString), isJSON: true }; + } catch (error) { + return { jsonObject: {}, isJSON: false }; + } +}; +module.exports = async (req, res) => { + try { + return res.status(200).json({ response: "response" }); + /* const { newText, diagramId } = req.body; + + // Retrieve documents for nodes, groups, and links + const nodesSnapshot = await db.collection("nodes").where("diagramId", "array-contains", diagramId).get(); + const groupsSnapshot = await db.collection("groups").where("diagramId", "array-contains", diagramId).get(); + const linksSnapshot = await db.collection("links").where("diagramId", "array-contains", diagramId).get(); + + // Process nodes and include doc.id so we can map IDs to labels. + const nodes = nodesSnapshot.docs.map(doc => { + const data = doc.data(); + data.id = doc.id; + delete data.diagrams; + return data; + }); + + // Build a map from node IDs to their labels + const nodeLabelMap = {}; + nodes.forEach(node => { + nodeLabelMap[node.id] = node.label; + }); + + // Process groups + const groups = groupsSnapshot.docs.map(doc => { + const data = doc.data(); + delete data.diagrams; + return data; + }); + + // Process links and replace source/target IDs with the corresponding node labels + const links = linksSnapshot.docs.map(doc => { + const data = doc.data(); + delete data.diagrams; + data.source = nodeLabelMap[data.source] || data.source; + data.target = nodeLabelMap[data.target] || data.target; + return data; + }); + + const previousDiagram = { nodes, groups, links }; + const promptDoc = await db.collection("diagramPrompts").doc("improve").get(); + const promptData = promptDoc.data(); + const llmPrompt = promptData.prompt; + const prompt = ` + ${llmPrompt} + + **Existing Causal Loop Diagram**: + ${previousDiagram} + + **New Text Data**: + ${newText}`; + + const completion = await openai.chat.completions.create({ + messages: [{ content: prompt, role: "user" }], + model: "o1", + temperature: 0 + }); + + const response = completion.choices[0].message.content; + + const isJSONObject = extractJSON(response || ""); + + if (!isJSONObject.isJSON) { + return res.status(400).json({ + message: "Invalid response from the model." + }); + } + + return res.status(200).json( + isJSONObject.jsonObject.modifications.map(modification => { + if (modification.level === "Node" && modification.type === "Modify") { + modification.targetId = nodes.find(node => node.label === modification.targetId).id; + } + if (modification.level === "Link" && modification.type === "Modify") { + modification.targetId = links.find(link => link.source === modification.targetId).id; + } + return modification; + }) + ); */ + } catch (e) { + console.error(e); + return res.status(500).json({ + message: e.message + }); + } +}; diff --git a/functions/app.js b/functions/app.js index 442cf68ba..d427d4aaa 100644 --- a/functions/app.js +++ b/functions/app.js @@ -46,6 +46,8 @@ const signUp = require("./api/signUp"); const researchersRouter = require("./api/researchers"); const participantsRouter = require("./api/participants"); const adminRouter = require("./api/administrator"); +const generateCollabDiagram = require("./api/generateCollabDiagram"); +const improveCollabDiagram = require("./api/improveCollabDiagram"); const EST_TIMEZONE = "America/Detroit"; process.env.TZ = EST_TIMEZONE; @@ -111,6 +113,10 @@ app.post("/recordAudio", recordAudio); app.post("/assignThematicPoints", assignThematicPoints); // Knowledge endpoints app.post("/inviteInstructors", inviteInstructors); + +app.post("/generateCollabDiagram", generateCollabDiagram); +app.post("/improveCollabDiagram", improveCollabDiagram); + // Misinformation Experiment // app.get("/card", card); // app.get("/image*", image); diff --git a/functions/helpers/openai.js b/functions/helpers/openai.js new file mode 100644 index 000000000..fdb04b411 --- /dev/null +++ b/functions/helpers/openai.js @@ -0,0 +1,11 @@ +const { OpenAI } = require("openai"); +require("dotenv").config(); + +const openai = new OpenAI({ + apiKey: process.env.API_KEY, + organization: process.env.ORG_ID +}); + +module.exports = { + openai +}; diff --git a/functions/helpers/send-prompt.js b/functions/helpers/send-prompt.js index 5828cda36..97ae5ecb2 100644 --- a/functions/helpers/send-prompt.js +++ b/functions/helpers/send-prompt.js @@ -1,16 +1,14 @@ -const { Configuration } = require("openai"); -const { OpenAIApi } = require("openai"); +const { OpenAI } = require("openai"); +require("dotenv").config(); -const configuration = new Configuration({ +const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY ?? "", organization: process.env.OPENAI_API_ORG_ID ?? "" }); -const openai = new OpenAIApi(configuration); - const sendPromptAndReceiveResponse = async ({ model, prompt }) => { try { - const completion = await openai.createChatCompletion({ + const completion = await openai.chat.completions({ model, temperature: 0, messages: [{ role: "user", content: prompt }] diff --git a/functions/package-lock.json b/functions/package-lock.json index 9992de7b6..9c180a42a 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -26,7 +26,7 @@ "moment-timezone": "^0.5.40", "nodemailer": "^6.6.5", "number-to-words": "^1.2.4", - "openai": "^3.2.1", + "openai": "^4.82.0", "typesense": "^1.3.0", "wink-porter2-stemmer": "^2.0.1", "wink-tokenizer": "^5.3.0" @@ -2598,6 +2598,15 @@ "undici-types": "~5.26.4" } }, + "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==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/qs": { "version": "6.9.10", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", @@ -2717,6 +2726,17 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -5576,6 +5596,23 @@ "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==" + }, + "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==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/formidable": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", @@ -6595,6 +6632,14 @@ "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==", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -9619,6 +9664,24 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "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" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -9782,12 +9845,40 @@ } }, "node_modules/openai": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz", - "integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==", + "version": "4.86.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.86.1.tgz", + "integrity": "sha512-x3iCLyaC3yegFVZaxOmrYJjitKxZ9hpVbLi+ZlT5UHuHTMlEQEbKXkGOM78z9qm2T5GF+XRUZCP2/aV4UPFPJQ==", "dependencies": { - "axios": "^0.26.0", - "form-data": "^4.0.0" + "@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.78", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.78.tgz", + "integrity": "sha512-m1ilZCTwKLkk9rruBJXFeYN0Bc5SbjirwYX/Td3MqPfioYbgun3IvK/m8dQxMCnrPGZPg1kvXjp3SIekCN/ynw==", + "dependencies": { + "undici-types": "~5.26.4" } }, "node_modules/ora": { @@ -12015,6 +12106,14 @@ "defaults": "^1.0.3" } }, + "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==", + "engines": { + "node": ">= 14" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -14386,6 +14485,15 @@ "undici-types": "~5.26.4" } }, + "@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "requires": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "@types/qs": { "version": "6.9.10", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", @@ -14487,6 +14595,14 @@ } } }, + "agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "requires": { + "humanize-ms": "^1.2.1" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -16734,6 +16850,20 @@ "mime-types": "^2.1.12" } }, + "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==" + }, + "formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "requires": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + } + }, "formidable": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", @@ -17523,6 +17653,14 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" }, + "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==", + "requires": { + "ms": "^2.0.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -19821,6 +19959,11 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "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==" + }, "node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -19934,12 +20077,27 @@ } }, "openai": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz", - "integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==", + "version": "4.86.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.86.1.tgz", + "integrity": "sha512-x3iCLyaC3yegFVZaxOmrYJjitKxZ9hpVbLi+ZlT5UHuHTMlEQEbKXkGOM78z9qm2T5GF+XRUZCP2/aV4UPFPJQ==", "requires": { - "axios": "^0.26.0", - "form-data": "^4.0.0" + "@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" + }, + "dependencies": { + "@types/node": { + "version": "18.19.78", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.78.tgz", + "integrity": "sha512-m1ilZCTwKLkk9rruBJXFeYN0Bc5SbjirwYX/Td3MqPfioYbgun3IvK/m8dQxMCnrPGZPg1kvXjp3SIekCN/ynw==", + "requires": { + "undici-types": "~5.26.4" + } + } } }, "ora": { @@ -21657,6 +21815,11 @@ "defaults": "^1.0.3" } }, + "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==" + }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/functions/package.json b/functions/package.json index 2c5441d03..a40e681cc 100644 --- a/functions/package.json +++ b/functions/package.json @@ -34,7 +34,7 @@ "moment-timezone": "^0.5.40", "nodemailer": "^6.6.5", "number-to-words": "^1.2.4", - "openai": "^3.2.1", + "openai": "^4.82.0", "typesense": "^1.3.0", "wink-porter2-stemmer": "^2.0.1", "wink-tokenizer": "^5.3.0" @@ -44,4 +44,4 @@ }, "private": true, "license": "Apache-2.0" -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ac66b50d1..265ac4653 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@mui/styles": "^5.2.1", "@mui/x-data-grid": "^5.0.1", "@mui/x-date-pickers": "^5.0.0-alpha.0", + "@mui/x-tree-view": "^7.26.0", "@nivo/bump": "^0.74.0", "@nivo/calendar": "^0.74.0", "@nivo/core": "^0.74.0", @@ -1989,9 +1990,9 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.4.tgz", - "integrity": "sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", + "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2417,13 +2418,13 @@ } }, "node_modules/@emotion/cache": { - "version": "11.13.1", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz", - "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.0", + "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } @@ -2527,9 +2528,9 @@ } }, "node_modules/@emotion/utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.0.tgz", - "integrity": "sha512-spEnrA1b6hDR/C68lC2M7m6ALPUHZC0lIY7jAS/B/9DuuO1ZP04eov8SMv/6fwRd8pzmsn2AuJEznRREWlQrlQ==" + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" }, "node_modules/@emotion/weak-memoize": { "version": "0.4.0", @@ -5054,15 +5055,15 @@ } }, "node_modules/@mui/lab": { - "version": "5.0.0-alpha.173", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.173.tgz", - "integrity": "sha512-Gt5zopIWwxDgGy/MXcp6GueD84xFFugFai4hYiXY0zowJpTVnIrTQCQXV004Q7rejJ7aaCntX9hpPJqCrioshA==", + "version": "5.0.0-alpha.175", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.175.tgz", + "integrity": "sha512-AvM0Nvnnj7vHc9+pkkQkoE1i+dEbr6gsMdnSfy7X4w3Ljgcj1yrjZhIt3jGTCLzyKVLa6uve5eLluOcGkvMqUA==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/base": "5.0.0-beta.40", - "@mui/system": "^5.16.5", + "@mui/base": "5.0.0-beta.40-0", + "@mui/system": "^5.16.12", "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.5", + "@mui/utils": "^5.16.12", "clsx": "^2.1.0", "prop-types": "^15.8.1" }, @@ -5077,9 +5078,9 @@ "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@mui/material": ">=5.15.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -5093,6 +5094,38 @@ } } }, + "node_modules/@mui/lab/node_modules/@mui/base": { + "version": "5.0.0-beta.40-0", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40-0.tgz", + "integrity": "sha512-hG3atoDUxlvEy+0mqdMpWd04wca8HKr2IHjW/fAjlkCHQolSLazhZM46vnHjOf15M4ESu25mV/3PgjczyjVM4w==", + "deprecated": "This package has been replaced by @base-ui-components/react", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.12", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", @@ -5138,12 +5171,12 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz", - "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==", + "version": "5.16.14", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.14.tgz", + "integrity": "sha512-12t7NKzvYi819IO5IapW2BcR33wP/KAVrU8d7gLhGHoAmhDxyXlRoKiRij3TOD8+uzk0B6R9wHUNKi4baJcRNg==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.16.6", + "@mui/utils": "^5.16.14", "prop-types": "^15.8.1" }, "engines": { @@ -5154,8 +5187,8 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -5164,12 +5197,12 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz", - "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==", + "version": "5.16.14", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.14.tgz", + "integrity": "sha512-UAiMPZABZ7p8mUW4akDV6O7N3+4DatStpXMZwPlt+H/dA0lt67qawN021MNND+4QTpjaiMYxbhKZeQcyWCbuKw==", "dependencies": { "@babel/runtime": "^7.23.9", - "@emotion/cache": "^11.11.0", + "@emotion/cache": "^11.13.5", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, @@ -5183,7 +5216,7 @@ "peerDependencies": { "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -5235,15 +5268,15 @@ } }, "node_modules/@mui/system": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", - "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", + "version": "5.16.14", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.14.tgz", + "integrity": "sha512-KBxMwCb8mSIABnKvoGbvM33XHyT+sN0BzEBG+rsSc0lLQGzs7127KWkCA6/H8h6LZ00XpBEME5MAj8mZLiQ1tw==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.16.6", - "@mui/styled-engine": "^5.16.6", + "@mui/private-theming": "^5.16.14", + "@mui/styled-engine": "^5.16.14", "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.6", + "@mui/utils": "^5.16.14", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -5258,8 +5291,8 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -5287,16 +5320,16 @@ } }, "node_modules/@mui/utils": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", - "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", + "version": "5.16.14", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.14.tgz", + "integrity": "sha512-wn1QZkRzSmeXD1IguBVvJJHV3s6rxJrfb6YuC9Kk6Noh9f8Fb54nUs5JRkKm+BOerRhj5fLg05Dhx/H3Ofb8Mg==", "dependencies": { "@babel/runtime": "^7.23.9", "@mui/types": "^7.2.15", "@types/prop-types": "^15.7.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^18.3.1" + "react-is": "^19.0.0" }, "engines": { "node": ">=12.0.0" @@ -5306,8 +5339,8 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -5315,6 +5348,11 @@ } } }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", + "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==" + }, "node_modules/@mui/x-data-grid": { "version": "5.17.26", "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-5.17.26.tgz", @@ -5414,6 +5452,62 @@ "node": ">=6" } }, + "node_modules/@mui/x-internals": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.26.0.tgz", + "integrity": "sha512-VxTCYQcZ02d3190pdvys2TDg9pgbvewAVakEopiOgReKAUhLdRlgGJHcOA/eAuGLyK1YIo26A6Ow6ZKlSRLwMg==", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-tree-view": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.26.0.tgz", + "integrity": "sha512-adZwVj6/edNowi2RIeyGPTcfdP4EXtMGo0mk2LQogG8m8bZkZRjOQoQ7pkBF0UPMaIAwzCadq2OWj3MPH4DP5A==", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0", + "@mui/x-internals": "7.26.0", + "@types/react-transition-group": "^4.4.11", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", diff --git a/package.json b/package.json index 922caa4fd..547dfaf5e 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@mui/styles": "^5.2.1", "@mui/x-data-grid": "^5.0.1", "@mui/x-date-pickers": "^5.0.0-alpha.0", + "@mui/x-tree-view": "^7.26.0", "@nivo/bump": "^0.74.0", "@nivo/calendar": "^0.74.0", "@nivo/core": "^0.74.0", diff --git a/src/Components/CollabTree/CollabTree.js b/src/Components/CollabTree/CollabTree.js new file mode 100644 index 000000000..8c0894f98 --- /dev/null +++ b/src/Components/CollabTree/CollabTree.js @@ -0,0 +1,114 @@ +import Box from "@mui/material/Box"; +import { SimpleTreeView } from "@mui/x-tree-view/SimpleTreeView"; +import { TreeItem } from "@mui/x-tree-view/TreeItem"; +import AddBoxIcon from "@mui/icons-material/AddBox"; +import IndeterminateCheckBoxIcon from "@mui/icons-material/IndeterminateCheckBox"; +import { Typography, IconButton } from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import AddIcon from "@mui/icons-material/Add"; +import { useState } from "react"; +import CheckBox from "@mui/material/Checkbox"; + +const CollabTree = ({ data, setData, setSelectedGroups, selectedGroups }) => { + const handleAddNode = parentId => { + setData(prevData => { + const newData = JSON.parse(JSON.stringify(prevData)); + + const addNode = nodes => { + nodes.forEach(node => { + if (node.id === parentId) { + if (!node.subgroups) node.subgroups = []; + node.subgroups.push({ id: Date.now().toString(), label: "New Node", subgroups: [] }); + } else if (node.subgroups) { + addNode(node.subgroups); + } + }); + }; + addNode(newData); + return newData; + }); + }; + + const handleDeleteNode = nodeId => { + setData(prevData => { + const newData = JSON.parse(JSON.stringify(prevData)); + + const deleteNode = (nodes, parent = null) => { + return nodes.filter(node => { + if (node.id === nodeId) return false; + if (node.subgroups) node.subgroups = deleteNode(node.subgroups, node); + return true; + }); + }; + + return deleteNode(newData); + }); + }; + + const renderTree = groups => ( + <> + {groups.map(group => ( + + { + e.stopPropagation(); + setSelectedGroups(prev => { + const _prev = new Set(prev); + if (_prev.has(group.id)) { + _prev.delete(group.id); + } else { + _prev.add(group.id); + } + return _prev; + }); + }} + checked={selectedGroups.has(group.id)} + sx={{ p: 0 }} + /> + {group.label} + {/* { + e.stopPropagation(); + handleAddNode(group.id); + }} + > + + + { + e.stopPropagation(); + handleDeleteNode(group.id); + }} + > + + */} + + } + nodeId={group.id} + slots={{ + expandIcon: group.subgroups && group.subgroups.length > 0 ? AddBoxIcon : IndeterminateCheckBoxIcon + }} + + /* sx={{ backgroundColor: selectedGroups.has(group.id) ? "#4caf50" : "", borderBlock: "1px solid gray" }} */ + > + {group.subgroups && group.subgroups.length > 0 ? renderTree(group.subgroups) : null} + + ))} + + ); + + return ( + + {renderTree(data)} + + ); +}; + +export default CollabTree; diff --git a/src/Components/OneCademyCollaborationModel.css b/src/Components/OneCademyCollaborationModel.css index 4f8f0f542..b9c1a447d 100644 --- a/src/Components/OneCademyCollaborationModel.css +++ b/src/Components/OneCademyCollaborationModel.css @@ -12,7 +12,6 @@ g.type-PO>rect { } .node rect { - stroke: #ecf3ed; fill: #fff; stroke-width: 1.5px; } @@ -24,5 +23,5 @@ g.type-PO>rect { } text.custom-text-color { - fill: #000000; -} + fill: #000000; +} \ No newline at end of file diff --git a/src/Components/OneCademyCollaborationModel.js b/src/Components/OneCademyCollaborationModel.js index 22cd35c72..891c9ad3c 100644 --- a/src/Components/OneCademyCollaborationModel.js +++ b/src/Components/OneCademyCollaborationModel.js @@ -1,5 +1,14 @@ -import React, { useRef, useEffect, useState } from "react"; +import React, { useRef, useEffect, useState, useCallback } from "react"; import AddIcon from "@mui/icons-material/Add"; +import { LoadingButton } from "@mui/lab"; +import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome"; +import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; +import AutoFixHighIcon from "@mui/icons-material/AutoFixHigh"; +import CircularProgress from "@mui/material/CircularProgress"; +import LinearProgress from "@mui/material/LinearProgress"; +import CloseIcon from "@mui/icons-material/Close"; +import WbSunnyIcon from "@mui/icons-material/WbSunny"; +import BedtimeIcon from "@mui/icons-material/Bedtime"; import { useRecoilValue } from "recoil"; import { emailState, firebaseState } from "../store/AuthAtoms"; import Button from "@mui/material/Button"; @@ -15,7 +24,7 @@ import FormControl from "@mui/material/FormControl"; import MenuItem from "@mui/material/MenuItem"; import Paper from "@mui/material/Paper"; import Grid from "@mui/material/Grid"; -import { Typography } from "@mui/material"; +import { Tooltip, Typography } from "@mui/material"; import IconButton from "@mui/material/IconButton"; import DeleteIcon from "@mui/icons-material/Delete"; import TrendingFlatIcon from "@mui/icons-material/TrendingFlat"; @@ -23,9 +32,7 @@ import MenuIcon from "@mui/icons-material/Menu"; import ListItem from "@mui/material/ListItem"; import ListItemText from "@mui/material/ListItemText"; import Checkbox from "@mui/material/Checkbox"; -import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"; import LegendToggleIcon from "@mui/icons-material/LegendToggle"; -import CloseIcon from "@mui/icons-material/Close"; import { useTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; import Drawer from "@mui/material/Drawer"; @@ -34,8 +41,12 @@ import DialogContent from "@mui/material/DialogContent"; import DialogTitle from "@mui/material/DialogTitle"; import { useThemeContext } from "../ThemeContext"; import FormControlLabel from "@mui/material/FormControlLabel"; -import Switch from "@mui/material/Switch"; import AddNodeTypeModal from "./CollaborativeModel/AddNodeTypeModal"; +import GetAppIcon from "@mui/icons-material/GetApp"; +import CollabTree from "./CollabTree/CollabTree"; +import useConfirmDialog from "../hooks/useConfirmDialog"; +import usePromptDialog from "../hooks/usePromptDialog"; +import axios from "axios"; const d3 = require("d3"); const legends = [ @@ -46,56 +57,78 @@ const legends = [ ]; // const NODE_TYPES = ["Positive Outcome", "Negative Outcome", "Design Features"]; -const getColor = (nodeType, nodeTypes) => { - return nodeTypes[nodeType]?.color || ""; +const getColor = (nodeType, nodeTypes, factor) => { + console.log( + "nodeTypes[nodeType.toLowerCase()]?.color", + nodeType.toLowerCase(), + nodeTypes[nodeType.toLowerCase()]?.color + ); + const color = nodeTypes[nodeType.toLowerCase()]?.color || ""; + return changeAlphaColor(color, factor); +}; + +const changeAlphaColor = (color, factor) => { + let hex = color.replace("#", ""); + if (hex.length === 3) + hex = hex + .split("") + .map(x => x + x) + .join(""); + if (hex.length === 6) + return `rgba(${parseInt(hex.slice(0, 2), 16)}, ${parseInt(hex.slice(2, 4), 16)}, ${parseInt( + hex.slice(4, 6), + 16 + )}, ${factor})`; + if (hex.length === 8) + return `rgba(${parseInt(hex.slice(0, 2), 16)}, ${parseInt(hex.slice(2, 4), 16)}, ${parseInt( + hex.slice(4, 6), + 16 + )}, ${(parseInt(hex.slice(6, 8), 16) / 255) * factor})`; + return hex; +}; + +const NODE_TYPES = "nodeTypes"; +const COLLAB_MODEL_NODES = "collabModelNodes"; +const GROUPS = "groups"; +const LINKS = "links"; +const NODES = "nodes"; +const DIAGRAMS = "diagrams"; +const LINKS_TYPES = { + "known positive": { text: "Known Positive Effect", color: "#4caf50" }, + "hypothetical positive": { text: "Hypothetical Positive Effect", color: "#1BBAE4" }, + "known negative": { text: "Known Negative Effect", color: "#A91BE4" }, + "hypothetical negative": { text: "Hypothetical Negative Effect", color: "#E4451B" } }; const OneCademyCollaborationModel = () => { const firebase = useRecoilValue(firebaseState); - const [openAddNode, setOpenAddNode] = useState(false); - const [title, setTitle] = useState(""); - const [type, setType] = useState("Positive Outcome"); - const [nodesChanges, setNodesChanges] = useState([]); - const [nodesLoded, setNodesLoded] = useState(false); const svgRef = useRef(); - const [allNodes, setAllNodes] = useState([]); - const [explanation, setExplanation] = useState(""); - const [label, setLabel] = useState(""); - const [explanationLink, setExplanationLink] = useState(""); - const [popoverType, setPopoverType] = useState(""); - const [childrenIds, setChildrenIds] = useState([]); - const [loadData, setLoadData] = useState(false); - const [selectedNode, setSelectedNode] = useState(""); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [visibleNodes, setVisibleNodes] = useState([]); - const [openModifyLink, setOpenModifyLink] = useState(false); - const [typeLink, setTypeLink] = useState(""); - const [selectedLink, setSelectedLink] = useState({}); - const [showAll, setShowAll] = useState(false); - const email = useRecoilValue(emailState); + const { darkMode, toggleTheme } = useThemeContext(); + + const [newNode, setNewNode] = useState(null); + + const [selectedLink, setSelectedLink] = useState(null); const [zoomState, setZoomState] = useState(null); - const [linkOrder, setLinkOrder] = useState(0); - const [stepLink, setStepLink] = useState(0); - const [maxDepth, setMaxDepth] = useState(0); const [openSideBar, setOpenSideBar] = useState(false); - const [ingnorOrder, setIngnorOrder] = useState(true); - const [deleteDialogLinkOpen, setDeleteDialogLinkOpen] = useState(false); - const [openLegend, setOpenLegend] = useState(false); - const [selectedDiagram, setSelectedDiagram] = useState({}); - const [newDiagramName, setNewDiagramName] = useState(""); - const [openEditModal, setOpenEditModal] = useState(false); - const [openAddModal, setOpenAddModal] = useState(false); - const [listOfDiagrams, setListOfDiagrams] = useState([]); - const [editingDiagram, setEditingDiagram] = useState(false); - const [editor, setEditor] = useState(true); - const { darkMode, toggleTheme } = useThemeContext(); + const [selectedDiagram, setSelectedDiagram] = useState({ id: "", title: "" }); const [nodeTypes, setNodeTypes] = useState({}); const [isModalAddTypeOpen, setIsModalAddTypeOpen] = useState(false); const [editNodeType, setEditNodeType] = useState(null); + const [groups, setGroups] = useState([]); + const [links, setLinks] = useState([]); + const [nodes, setNodes] = useState({}); + const [selectedGroups, setSelectedGroups] = useState(new Set()); + const [diagrams, setDiagrams] = useState([]); - const theme = useTheme(); + const { promptIt, ConfirmDialog, confirmIt } = useConfirmDialog(); + const { promptItDiagram, PromptDialog } = usePromptDialog(); + const [loadingResponse, setLoadingResponse] = useState(false); + const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + + const editor = true; + const ColorBox = props => { return ( { ); }; - function addPencilButton(edgeElement, edgeData, pencilButtonsGroup, order) { + const addPencilButton = (edgeElement, edgeData, pencilButtonsGroup) => { let edgeLabel = edgeElement.select("path"); let edgePath = edgeLabel.node(); let pathLength = edgePath.getTotalLength(); @@ -161,25 +194,23 @@ const OneCademyCollaborationModel = () => { .text("✏️") .on("click", function (e) { e.stopPropagation(); - setSelectedLink({}); modifyLink(edgeData); }); - // pencilButtonsGroup - // .append("text") - // .attr("class", "custom-text-color") - // .attr("x", point.x) - // .attr("y", point.y + 14) - // .attr("font-family", "Arial, sans-serif") - // .attr("font-size", "15px") - // .text(order); - } + pencilButtonsGroup + .append("text") + .attr("class", "custom-text-color") + .attr("x", point.x) + .attr("y", point.y + 14) + .attr("font-family", "Arial, sans-serif") + .attr("font-size", "15px"); + }; useEffect(() => { - const unsubscribe = firebase.db.collection("nodeTypes").onSnapshot(snapshot => { + const unsubscribe = firebase.db.collection(NODE_TYPES).onSnapshot(snapshot => { const newNodeTypes = {}; snapshot.forEach(doc => { const data = doc.data(); - newNodeTypes[data.type] = { ...doc.data(), id: doc.id }; + newNodeTypes[data.type.toLowerCase()] = { ...doc.data(), id: doc.id }; }); setNodeTypes(newNodeTypes); }); @@ -189,14 +220,14 @@ const OneCademyCollaborationModel = () => { const saveNewType = async (type, color, editedNodeType) => { if (editedNodeType) { - const nodeTypeRef = firebase.db.collection("nodeTypes").doc(editedNodeType.id); + const nodeTypeRef = firebase.db.collection(NODE_TYPES).doc(editedNodeType.id); nodeTypeRef.update({ type, color }); const nodesDocsSnapshot = await firebase.db - .collection("collabModelNodes") + .collection(COLLAB_MODEL_NODES) .where("type", "==", editedNodeType.type) .get(); @@ -208,194 +239,144 @@ const OneCademyCollaborationModel = () => { await batch.commit(); } else { - const newTypeRef = firebase.db.collection("nodeTypes").doc(); + const newTypeRef = firebase.db.collection(NODE_TYPES).doc(); await newTypeRef.set({ type: type, color }); } }; - - useEffect(() => { - const _listOfDiagrams = [...listOfDiagrams]; - const diagramsListQuery = firebase.db.collection("collabModelDiagrams"); - const diagramsListcSnapshot = diagramsListQuery.onSnapshot(snapshot => { - const changes = snapshot.docChanges(); - for (let change of changes) { - if (change.type === "added") { - const findIndex = _listOfDiagrams.findIndex(diagram => diagram.id === change.doc.id); - if (findIndex === -1) { - _listOfDiagrams.push({ ...change.doc.data(), id: change.doc.id }); - } else { - _listOfDiagrams[findIndex] = { ...change.doc.data(), id: change.doc.id }; - } - } else if (change.type === "modified") { - const findIndex = _listOfDiagrams.findIndex(diagram => diagram.id === change.doc.id); - if (findIndex !== -1) { - _listOfDiagrams[findIndex] = { - ...change.doc.data(), - id: change.doc.id - }; - } - } else if (change.type === "removed") { - const findIndex = _listOfDiagrams.findIndex(diagram => diagram.id === change.doc.id); - if (findIndex !== -1) { - _listOfDiagrams.splice(findIndex, 1); - } - } - } - setListOfDiagrams(_listOfDiagrams); - if (Object.keys(selectedDiagram).length > 0) { - const _indexD = _listOfDiagrams.findIndex(diagram => diagram.id === selectedDiagram.id); - if (_indexD !== -1) { - setVisibleNodes(_listOfDiagrams[_indexD]?.nodes || []); - setSelectedDiagram(listOfDiagrams[_indexD] || {}); + const processGroupChanges = (prev, changes, object = false) => { + const _prev = object ? { ...prev } : [...prev]; + console.log("changes ==>", changes); + changes.forEach(change => { + const index = object ? -1 : _prev.findIndex(group => group.id === change.doc.id); + if (change.type === "added" || change.type === "modified") { + if (object) { + _prev[change.doc.id] = { ...change.doc.data(), id: change.doc.id }; + } else if (index === -1) { + _prev.push({ ...change.doc.data(), id: change.doc.id }); } else { - setSelectedDiagram(_listOfDiagrams[0] || {}); - setVisibleNodes(_listOfDiagrams[0]?.nodes || []); + _prev[index] = { ...change.doc.data(), id: change.doc.id }; + } + } else if (change.type === "removed") { + if (object) { + delete _prev[change.doc.id]; + } else if (index !== -1) { + _prev.splice(index, 1); } - } else { - setSelectedDiagram(_listOfDiagrams[0] || {}); - setVisibleNodes(_listOfDiagrams[0]?.nodes || []); } }); + return _prev; + }; + + useEffect(() => { + const diagramsQuery = firebase.db.collection(DIAGRAMS); + const unsubscribeDiagrams = diagramsQuery.onSnapshot(snapshot => { + const changes = snapshot.docChanges(); + setDiagrams(prev => processGroupChanges(prev, changes)); + }); return () => { - diagramsListcSnapshot(); + unsubscribeDiagrams(); }; - }, [firebase, selectedDiagram]); + }, [firebase.db]); useEffect(() => { - let collabModelNodesQuery = firebase.db.collection("collabModelNodes"); - const _allNodes = [...allNodes]; - const collabModelNodesSnapshot = collabModelNodesQuery.onSnapshot(snapshot => { + console.log("selectedDiagram?.id =====>", selectedDiagram?.id); + if (!selectedDiagram?.id) { + console.log(diagrams, "diagrams"); + setSelectedDiagram(diagrams[0]); + } + }, [diagrams, selectedDiagram?.id]); + + useEffect(() => { + if (!selectedDiagram?.id) return; + + const groupsQuery = firebase.db.collection(GROUPS).where("diagrams", "array-contains", selectedDiagram.id); + const linksQuery = firebase.db.collection(LINKS).where("diagrams", "array-contains", selectedDiagram.id); + const nodesQuery = firebase.db.collection(NODES).where("diagrams", "array-contains", selectedDiagram.id); + + const unsubscribeGroups = groupsQuery.onSnapshot(snapshot => { const changes = snapshot.docChanges(); - for (let change of changes) { - const data = change.doc.data(); - if (change.type === "added") { - if (!data.deleted) { - const findIndex = _allNodes.findIndex(node => node.id === change.doc.id); - if (findIndex === -1) { - _allNodes.push({ id: change.doc.id, ...data }); - } else { - _allNodes[findIndex] = { id: change.doc.id, ...data }; - } - } else if (data.deleted) { - const findIndex = _allNodes.findIndex(node => node.id === change.doc.id); - if (findIndex !== -1) { - _allNodes.splice(findIndex, 1); - } - } - } else if (change.type === "modified") { - if (!data.deleted) { - const findIndex = _allNodes.findIndex(node => node.id === change.doc.id); - if (findIndex !== -1) { - _allNodes[findIndex] = { id: change.doc.id, ...data }; - } - } else if (data.deleted) { - const findIndex = _allNodes.findIndex(node => node.id === change.doc.id); - if (findIndex !== -1) { - _allNodes.splice(findIndex, 1); - } - } - } else if (change.type === "removed") { - const findIndex = _allNodes.findIndex(node => node.id === change.doc.id); - if (findIndex !== -1) { - _allNodes.splice(findIndex, 1); - } - } - } - setNodesChanges(oldChanges => [...oldChanges, ...changes]); - setNodesLoded(true); - setLoadData(false); - _allNodes.sort((a, b) => (a.title > b.title ? 1 : -1)); - setAllNodes(_allNodes); - setShowAll(_allNodes.length === visibleNodes.length); + setGroups(prev => processGroupChanges(prev, changes)); + }); + + const unsubscribeLinks = linksQuery.onSnapshot(snapshot => { + const changes = snapshot.docChanges(); + setLinks(prev => processGroupChanges(prev, changes)); }); + + const unsubscribeNodes = nodesQuery.onSnapshot(snapshot => { + const changes = snapshot.docChanges(); + setNodes(prev => processGroupChanges(prev, changes, true)); + }); + return () => { - collabModelNodesSnapshot(); - setNodesLoded(false); - setLoadData(false); + unsubscribeGroups(); + unsubscribeLinks(); + unsubscribeNodes(); }; - }, [firebase, loadData, selectedDiagram]); - - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(async () => { - if (!allNodes.length) return; - const researchersDoc = await firebase.db.collection("researchers").where("email", "==", email).get(); - if (researchersDoc.docs.length) { - const resData = researchersDoc.docs[0].data(); - setEditor(resData.hasOwnProperty("isEditor") && resData.isEditor); - } else { - setEditor(false); - } + }, [firebase.db, selectedDiagram?.id]); + console.log("groups", groups, links, nodes); + + useEffect(() => { var g = new dagreD3.graphlib.Graph({ compound: true }) .setGraph({ rankdir: "LR", isMultigraph: true }) .setDefaultEdgeLabel(function () { return {}; }); d3.select("#graphGroup").selectAll("*").remove(); - setNodesChanges([]); - for (let collabModelNode of allNodes) { - if ( - visibleNodes.includes(collabModelNode.id) && - !collabModelNode.deleted && - !g.nodes().includes(collabModelNode.id) - ) { - g.setNode(collabModelNode.id, { - label: collabModelNode.title, - class: - selectedNode === collabModelNode.id - ? "type-ST" - : collabModelNode.type === "Negative Outcome" - ? "type-NO" - : collabModelNode.type === "Positive Outcome" - ? "type-PO" - : "type-DF", - style: `fill: ${getColor(collabModelNode.type, nodeTypes)}` + console.log("useEffect", { + nodes, + darkMode, + nodeTypes + }); + + for (let visibleNodeId in nodes) { + if (nodes[visibleNodeId] && !g.nodes().includes(visibleNodeId)) { + const nodeData = nodes[visibleNodeId]; + const isBlurred = !selectedGroups.has(nodeData.groups[0].id) && !newNode; + let nodeColor = getColor(nodeData.nodeType, nodeTypes, isBlurred ? 0.1 : 1); + const textColor = changeAlphaColor(!darkMode && isBlurred ? "#000000" : "#fff", isBlurred ? 0.1 : 1); + + g.setNode(nodeData.id, { + label: nodeData.label, + class: `${"type-PO"}`, + style: `fill: ${nodeColor};`, + labelStyle: `fill: ${textColor};` }); } } - let _maxDepth = 0; - for (let collabModelNode of allNodes) { - if (!collabModelNode.children || !collabModelNode.children.length) continue; - for (let elementChild of collabModelNode.children) { - if (_maxDepth < elementChild?.order) { - _maxDepth = elementChild?.order; + + for (let { source, target, polarity, certainty } of links) { + if (target && source) { + const isHighlighted = + (selectedGroups.has(nodes[source]?.groups[0]?.id) && selectedGroups.has(nodes[target]?.groups[0]?.id)) || + !!newNode; + const color = LINKS_TYPES[`${certainty.trim()} ${polarity.trim()}`]?.color || ""; + + const adjustedColor = changeAlphaColor(color, isHighlighted ? 1 : 0.1); + + let _arrowheadStyle = `fill: ${adjustedColor};`; + let _style = `stroke: ${adjustedColor}; stroke-width: 2px;`; + + if (selectedLink?.v === source && selectedLink?.w === target) { + _style = "stroke: #212121; stroke-width: 3px; filter: drop-shadow(3px 3px 5px #212121); opacity: 1;"; + _arrowheadStyle = "fill: #212121; opacity: 1;"; } - if ( - !elementChild.deleted && - visibleNodes.includes(elementChild.id) && - visibleNodes.includes(collabModelNode.id) && - !collabModelNode.deleted - ) { - let color = legends.find(legend => legend.text === elementChild.type)?.color || ""; - let _arrowheadStyle = `fill: ${color}`; - let _style = `stroke: ${color}; stroke-width: 2px;`; - // if (parseInt(elementChild.order) === stepLink && stepLink !== 0) { - // _style = `stroke:${color}; stroke-width: 3px;filter: drop-shadow(3px 3px 5px ${color}); stroke-width: 2.7px;`; - // } - if (selectedLink.v === collabModelNode.id && selectedLink.w === elementChild.id) { - _style = - "stroke: #212121; stroke-width: 3px; filter: drop-shadow(3px 3px 5px #212121); stroke-width: 2.7px;"; - _arrowheadStyle = "fill: #212121"; - } - if ( - ingnorOrder || - showAll || - (parseInt(elementChild.order) > 0 && parseInt(elementChild.order) <= stepLink) - ) { - g.setEdge(collabModelNode.id, elementChild.id, { - curve: d3.curveBasis, - style: _style, - arrowheadStyle: _arrowheadStyle, - label: elementChild?.label || "" - }); - } + + if (g.nodes().includes(source) && g.nodes().includes(target)) { + g.setEdge(source, target, { + curve: d3.curveBasis, + style: _style, + arrowheadStyle: _arrowheadStyle, + label: "" + }); } } } - setMaxDepth(_maxDepth); + g.nodes().forEach(function (v) { var node = g.node(v); if (node) { @@ -434,32 +415,33 @@ const OneCademyCollaborationModel = () => { let buttonBody = button.append("xhtml:body").style("margin", "0px").style("padding", "0px"); - buttonBody - .append("xhtml:button") - .style("background", "white") - .style("color", "black") - .style("border", "1px solid black") - .style("border-radius", "50%") - .style("width", "20px") - .style("height", "20px") - .style("font-weight", "bold") - .style("font-size", "12px") - .style("line-height", "18px") - .style("text-align", "center") - .style("padding", "0") - .text("X") - .on("click", function (e) { - e.stopPropagation(); - removeNode(v); - }) - .on("mouseover", function () { - d3.select(this).style("background", "orange"); - }) - .on("mouseout", function () { - d3.select(this).style("background", "white"); - }); - - if (selectedNode || openAddNode) { + /* if (editor) { + buttonBody + .append("xhtml:button") + .style("background", "white") + .style("color", "black") + .style("border", "1px solid black") + .style("border-radius", "50%") + .style("width", "20px") + .style("height", "20px") + .style("font-weight", "bold") + .style("font-size", "12px") + .style("line-height", "18px") + .style("text-align", "center") + .style("padding", "0") + .text("X") + .on("click", function (e) { + e.stopPropagation(); + removeNode(v); + }) + .on("mouseover", function () { + d3.select(this).style("background", "orange"); + }) + .on("mouseout", function () { + d3.select(this).style("background", "white"); + }); + } */ + if (!!newNode) { let button2 = nodeElement .append("foreignObject") .attr("width", 20) @@ -469,25 +451,49 @@ const OneCademyCollaborationModel = () => { .attr("class", "hide-button"); var buttonBody2 = button2.append("xhtml:body").style("margin", "0px").style("padding", "0px"); - if (childrenIds.includes(v)) { + const tooltip = d3 + .select("body") + .append("div") + .attr("class", "tooltip") + .style("position", "absolute") + .style("visibility", "hidden") + .style("background-color", "rgba(0,0,0,0.7)") + .style("color", "white") + .style("padding", "5px") + .style("border-radius", "4px") + .style("font-size", "12px"); + + if (newNode.children.includes(v)) { buttonBody2 .append("xhtml:button") .style("background", "white") - .style("color", "black") .style("border", "1px solid black") .style("border-radius", "50%") .style("width", "20px") .style("height", "20px") - .style("font-weight", "bold") .style("font-size", "25px") - .style("line-height", "2px") + .style("line-height", "10px") .style("padding", "0") .style("text-align", "center") .style("cursor", "pointer") + .style("padding-bottom", "3px") .text("-") .on("click", function (e) { e.stopPropagation(); removeChild(v); + tooltip.style("visibility", "hidden"); + }) + .on("mouseover", function (event) { + d3.select(this).style("background", "#FFCC80"); // light orange + tooltip + .style("visibility", "visible") + .text("Remove as child") + .style("left", event.pageX + 10 + "px") + .style("top", event.pageY + 10 + "px"); + }) + .on("mouseout", function () { + d3.select(this).style("background", "white"); + tooltip.style("visibility", "hidden"); }); } else { buttonBody2 @@ -498,7 +504,6 @@ const OneCademyCollaborationModel = () => { .style("border-radius", "50%") .style("width", "20px") .style("height", "20px") - .style("font-weight", "bold") .style("font-size", "25px") .style("line-height", "2px") .style("padding", "0") @@ -508,6 +513,19 @@ const OneCademyCollaborationModel = () => { .on("click", function (e) { e.stopPropagation(); addChild(v); + tooltip.style("visibility", "hidden"); + }) + .on("mouseover", function (event) { + d3.select(this).style("background", "orange"); + tooltip + .style("visibility", "visible") + .text("Add as child") + .style("left", event.pageX + 10 + "px") + .style("top", event.pageY + 10 + "px"); + }) + .on("mouseout", function () { + d3.select(this).style("background", "white"); + tooltip.style("visibility", "hidden"); }); } } @@ -537,9 +555,9 @@ const OneCademyCollaborationModel = () => { svg.call(zoom.transform, d3.zoomIdentity.translate(translateX, translateY).scale(zoomScale)); } - const nodes = svg.selectAll("g.node"); + const gNodes = svg.selectAll("g.node"); if (editor) { - nodes.on("click", function (d) { + gNodes.on("click", function (d) { d.stopPropagation(); modifyNode(d.target.__data__); }); @@ -548,1249 +566,920 @@ const OneCademyCollaborationModel = () => { if (editor) { edges.each(function (edgeData) { var edgeElement = d3.select(this); - const nodeIdx = allNodes.findIndex(node => node.id === edgeData.v); - const childIdx = allNodes[nodeIdx].children.findIndex(child => child.id === edgeData.w); - const order = allNodes[nodeIdx].children[childIdx].order; - addPencilButton(edgeElement, edgeData, pencilButtonsGroup, order); + const nodeData = nodes[edgeData.v]; + if (!nodeData) return; + /* const childIdx = nodeData.children.findIndex(child => child.id === edgeData.w); + if (childIdx === -1) return; + const order = nodeData.children[childIdx].order; */ + addPencilButton(edgeElement, edgeData, pencilButtonsGroup); }); } if (!editor) { edges.on("click", function (d) { - setSelectedLink({}); + setSelectedLink(null); showLinkDetails(d.target.__data__); - setOpenModifyLink(true); - setOpenAddNode(false); }); } - setNodesLoded(false); return () => { d3.select("#graphGroup").selectAll("*").remove(); setZoomState(null); }; - }, [nodesLoded, allNodes, visibleNodes, darkMode, nodeTypes]); + }, [selectedLink, darkMode, nodeTypes, nodes, links, selectedGroups, newNode]); const AddNewNode = second => { - setTitle(""); - setType(""); - setChildrenIds([]); - setSelectedNode(""); - setSelectedLink({}); - setDeleteDialogOpen(false); - setOpenAddNode(true); - setLoadData(true); - setOpenModifyLink(false); + setSelectedLink(null); + setNewNode({ + label: "", + nodeType: "process variable", + isLeverage: false, + groups: [], + diagrams: [], + children: [], + nodeType: "", + new: true + }); }; const handleClose = () => { - setTitle(""); - setType(""); - setChildrenIds([]); - setSelectedNode(""); - setOpenAddNode(false); - setSelectedLink({}); - setDeleteDialogOpen(false); - setLoadData(true); + setSelectedLink(null); + setNewNode(null); }; const handleSave = async () => { - const _visibleNodes = [...visibleNodes]; try { - if (!selectedNode) { - const children = []; - for (let child of childrenIds) { - children.push({ - id: child, - explanation: "", - type: "Hypothetical Positive Effect", - order: 0 - }); - _visibleNodes.push(child); - } - const collabModelRef = firebase.firestore().collection("collabModelNodes").doc(); - await collabModelRef.set({ - title, - type: type, - children + if (newNode?.new) { + const newNodeRef = firebase.db.collection(NODES).doc(); + newNode.groups = newNode.groups.map(c => { + return { + label: c.label, + id: c.id + }; }); - _visibleNodes.push(collabModelRef.id); - } else { - const collabModelRef = firebase.firestore().collection("collabModelNodes").doc(selectedNode); - const collabModelDoc = await collabModelRef.get(); - const collabModelNode = collabModelDoc.data(); - for (let childId of childrenIds) { - const idex = collabModelNode.children.findIndex(_child => _child.id === childId); - if (idex === -1) { - collabModelNode.children.push({ - id: childId, - explanation: "", - type: "Hypothetical Positive Effect", - order: 0 - }); - } else if ( - idex !== -1 && - collabModelNode.children[idex].hasOwnProperty("deleted") && - collabModelNode.children[idex]?.deleted - ) { - collabModelNode.children[idex].deleted = false; - } - if (_visibleNodes.includes(childId) && !collabModelNode.children[idex]?.deleted) { - _visibleNodes.push(childId); + + await newNodeRef.set({ + label: newNode.label, + nodeType: newNode.nodeType, + diagrams: [selectedDiagram.id], + groups: newNode.groups, + id: newNodeRef.id + }); + for (let child of newNode.children) { + const newLinkRef = firebase.db.collection("links").doc(); + const newLink = { + source: newNodeRef.id, + target: child, + certainty: "known", + polarity: "positive", + diagrams: [selectedDiagram.id], + detail: "" + }; + newLinkRef.set(newLink); + } + } else if (!!newNode.previous) { + const previousChildren = links.filter(c => c.source === newNode.id); + for (let previous of previousChildren) { + if (!newNode.children.includes(previous.target)) { + const linkRef = firebase.db.collection(LINKS).doc(previous.id); + linkRef.delete(); } } - for (let child of collabModelNode.children) { - if (!childrenIds.some(childId => childId === child.id)) { - for (let _child of collabModelNode.children) { - if (_child.id === child.id) continue; - if (_child.order > child.order && child.order !== 0) { - _child.order = parseInt(_child.order) - 1; - } - } - if (child.order > 0) { - for (let node of allNodes) { - if (node.id === selectedNode) continue; - const _children = node.children; - for (let _child of _children) { - if (_child.order >= child.order) { - _child.order = parseInt(_child.order) - 1; - } - } - const nodeRef = firebase.firestore().collection("collabModelNodes").doc(node.id); - nodeRef.update({ children: _children }); - } - } - child.deleted = true; - child.order = 0; + for (let child of newNode.children) { + const indx = previousChildren.findIndex(c => c.target === child); + if (indx === -1) { + const newLinkRef = firebase.db.collection("links").doc(); + const newLink = { + source: newNode.id, + target: child, + certainty: "known", + polarity: "positive", + diagrams: [selectedDiagram.id], + detail: "" + }; + newLinkRef.set(newLink); } } - await collabModelRef.update({ title, type, children: collabModelNode.children }); + const newNodeRef = firebase.db.collection(NODES).doc(newNode.id); + + newNode.groups = newNode.groups.map(c => { + return { + label: c.label, + id: c.id + }; + }); + await newNodeRef.update({ + label: newNode.label, + nodeType: newNode.nodeType, + diagrams: [selectedDiagram.id], + groups: newNode.groups, + id: newNodeRef.id + }); } - const _listOfDiagrams = [...listOfDiagrams]; - const _diagram = _listOfDiagrams.findIndex(diagram => diagram.id === selectedDiagram.id); - const diagramRef = firebase.db.collection("collabModelDiagrams").doc(selectedDiagram.id); - _listOfDiagrams[_diagram].nodes = [...new Set(_visibleNodes)]; - await diagramRef.update({ nodes: _listOfDiagrams[_diagram].nodes }); - } catch (error) {} - - setVisibleNodes([...new Set(_visibleNodes)]); - setOpenAddNode(false); - setLoadData(true); - setTitle(""); - setType(""); - setChildrenIds([]); - setSelectedNode(""); - setOpenAddNode(false); - setDeleteDialogOpen(false); + setNewNode(null); + } catch (error) { + console.error(error); + } }; const modifyNode = async nodeId => { - const nodeDoc = await firebase.firestore().collection("collabModelNodes").doc(nodeId).get(); - const node = nodeDoc.data(); - const childrenNodes = await firebase.firestore().collection("collabModelNodes").get(); - const _children = []; - for (let _nodeDoc of childrenNodes.docs) { - if (node.children.some(child => child.id === _nodeDoc.id && !child.deleted)) { - _children.push(_nodeDoc.id); + const children = []; + for (let link of links) { + if (link.source === nodeId) { + children.push(link.target); } } - setChildrenIds(_children); - setTitle(node.title); - setType(node.type); - setSelectedNode(nodeId); - setSelectedLink({}); - setOpenAddNode(true); - setOpenModifyLink(false); - setLoadData(true); + setNewNode({ ...nodes[nodeId], children, previous: true }); + setSelectedLink(null); }; + console.log(newNode, "newNode ==>"); const deleteNode = async () => { - if (!selectedNode) return; - const _allnodes = [...allNodes]; - const _listOfDiagrams = [...listOfDiagrams]; - const _diagram = _listOfDiagrams.findIndex(diagram => diagram.id === selectedDiagram.id); - const diagramRef = firebase.db.collection("collabModelDiagrams").doc(selectedDiagram.id); - let _visibleNodes = [...visibleNodes]; - for (let node of _allnodes) { - if (node.id === selectedNode) { - node.deleted = true; - } - const index = node.children.findIndex(child => child.id === selectedNode && !child?.deleted); - if (index !== -1) { - const oldOrder = node.children[index].order; - node.children[index].deleted = true; - node.children[index].order = 0; - for (let node of _allnodes) { - for (let _child of node.children) { - if (_child.order > oldOrder && !_child?.deleted) { - _child.order = parseInt(_child.order) - 1; - } - } - } - } - } - await firebase.db.runTransaction(async t => { - for (let node of _allnodes) { - const nodeRef = firebase.firestore().collection("collabModelNodes").doc(node.id); - t.update(nodeRef, { children: node.children, deleted: node?.deleted || false }); - } - }); - _visibleNodes = _visibleNodes.filter(node => node !== selectedNode); - if (_diagram !== -1 && editor) { - _listOfDiagrams[_diagram].nodes = [..._visibleNodes]; - await diagramRef.update({ nodes: [...new Set(_visibleNodes)] }); + if (!newNode) return; + if (await confirmIt("Are you sure you want to delete this node?", "Delete", "Keep")) { + const nodeRef = firebase.db.collection(NODES).doc(newNode.id); + nodeRef.delete(); + setNewNode(null); } - setVisibleNodes(_visibleNodes); - setOpenAddNode(false); - setDeleteDialogOpen(false); - setLoadData(true); - setSelectedNode(""); }; const showLinkDetails = async data => { - const nodeId = data.v; + /* const nodeId = data.v; const childId = data.w; - const nodeDoc = await firebase.firestore().collection("collabModelNodes").doc(nodeId).get(); + const nodeDoc = await firebase.firestore().collection(COLLAB_MODEL_NODES).doc(nodeId).get(); const nodeData = nodeDoc.data(); const children = nodeData.children; const child = children.find(child => child.id === childId); setExplanationLink(child.explanation); - setPopoverType(child.type); if (child.explanation !== "") { setSelectedLink(data); - } - setLoadData(true); - }; - - const handleVisibileNodes = async node => { - let _visibleNodes = visibleNodes; - setShowAll(false); - setIngnorOrder(true); - const _listOfDiagrams = [...listOfDiagrams]; - const _diagram = _listOfDiagrams.findIndex(diagram => diagram.id === selectedDiagram.id); - const diagramRef = firebase.db.collection("collabModelDiagrams").doc(selectedDiagram.id); - - if (_visibleNodes.includes(node.id)) { - _visibleNodes.splice(_visibleNodes.indexOf(node.id), 1); - } else { - _visibleNodes.push(node.id); - const childrens = node.children.filter(child => !child?.deleted); - const parents = allNodes.filter(_node => _node.children.some(child => child.id === node.id && !child?.deleted)); - _visibleNodes = [..._visibleNodes, ...childrens.map(child => child.id), ...parents.map(parent => parent.id)]; - } - - let _showall = true; - for (let node of allNodes) { - if (!_visibleNodes.includes(node.id)) { - _showall = false; - break; - } - } - if (_diagram !== -1 && editor) { - _listOfDiagrams[_diagram].nodes = [..._visibleNodes]; - await diagramRef.update({ nodes: [...new Set(_visibleNodes)] }); - } - setListOfDiagrams(_listOfDiagrams); - // setZoomState(null); - setShowAll(_showall); - setVisibleNodes([...new Set(_visibleNodes)]); - setLoadData(true); - setStepLink(0); - }; - - const handleCloseLink = () => { - setOpenModifyLink(false); - setExplanation(""); - setTypeLink(""); - setSelectedLink({}); - setLoadData(true); - setLinkOrder(0); + } */ }; const modifyLink = async data => { + console.log(data, "data== >"); const nodeId = data.v; const childId = data.w; - const nodeDoc = await firebase.firestore().collection("collabModelNodes").doc(nodeId).get(); - const nodeData = nodeDoc.data(); - const children = nodeData.children; - const child = children.find(child => child.id === childId); - setExplanation(child.explanation); - setTypeLink(child.type); - setSelectedLink(data); - setLinkOrder(child.order); - setOpenModifyLink(true); - setOpenAddNode(false); - setLoadData(true); - setSelectedNode(""); + const link = links.find(link => link.source === nodeId && link.target === childId); + + setSelectedLink({ ...link, ...data }); + setNewNode(null); }; const handleSaveLink = async () => { await firebase.db.runTransaction(async t => { - const nodeId = selectedLink.v; - const childId = selectedLink.w; - const nodeRef = firebase.firestore().collection("collabModelNodes").doc(nodeId); - const nodeDoc = await t.get(nodeRef); - const nodeData = nodeDoc.data(); - const children = nodeData.children; - const child = children.find(child => child.id === childId); - if (child.order !== linkOrder) { - if (child.order === 0 || Number.isNaN(child.order)) { - for (let node of allNodes) { - const _children = node.children; - for (let _child of _children) { - if (_child.order >= linkOrder) { - _child.order = parseInt(_child.order) + 1; - } - if (nodeId === node.id && _child.id === child.id) { - _child.order = linkOrder; - _child.explanation = explanation; - _child.label = label; - _child.type = typeLink; - } - } - const nodeRef = firebase.firestore().collection("collabModelNodes").doc(node.id); - t.update(nodeRef, { children: _children }); - } - } else if (linkOrder === 0) { - for (let node of allNodes) { - const _children = node.children; - for (let _child of _children) { - if (_child.order >= child.order) { - _child.order = parseInt(_child.order) - 1; - } - if (nodeId === node.id && _child.id === child.id) { - _child.order = linkOrder; - _child.explanation = explanation; - _child.label = label; - _child.type = typeLink; - } - } - const nodeRef = firebase.firestore().collection("collabModelNodes").doc(node.id); - t.update(nodeRef, { children: _children }); - } - } else { - for (let node of allNodes) { - const _children = node.children; - - if (child.order < linkOrder) { - for (let _child of _children) { - if (_child.order === 7) { - debugger; - } - if (_child.order > child.order && _child.order <= linkOrder) { - _child.order = parseInt(_child.order) - 1; - } else if (_child.order === child.order) { - _child.order = linkOrder; - _child.explanation = explanation; - _child.label = label; - _child.type = typeLink; - } - } - } - if (linkOrder < child.order) { - for (let _child of _children) { - if (_child.order >= linkOrder && _child.order < child.order) { - _child.order = parseInt(_child.order) + 1; - } else if (_child.order === child.order) { - _child.order = linkOrder; - _child.explanation = explanation; - _child.label = label; - _child.type = typeLink; - } - } - } - const nodeRef = firebase.firestore().collection("collabModelNodes").doc(node.id); - t.update(nodeRef, { children: _children }); - } - } - } else if (child.order === linkOrder) { - const nodeRef = firebase.firestore().collection("collabModelNodes").doc(nodeId); - const _children = nodeData.children; - const child = _children.find(child => child.id === childId); - child.explanation = explanation; - child.type = typeLink; - child.label = label; - t.update(nodeRef, { children: _children }); - } - if (linkOrder > stepLink) { - setStepLink(linkOrder); - } - setLinkOrder(0); - setOpenModifyLink(false); - setSelectedLink({}); - setExplanation(""); - setTypeLink(""); - setLoadData(true); - }); - }; - - const removeNode = async nodeId => { - const _visibleNodes = visibleNodes; - const _listOfDiagrams = [...listOfDiagrams]; - const _diagram = _listOfDiagrams.findIndex(diagram => diagram.id === selectedDiagram.id); - const diagramRef = firebase.db.collection("collabModelDiagrams").doc(listOfDiagrams[_diagram].id); - if (_visibleNodes.includes(nodeId)) { - _visibleNodes.splice(_visibleNodes.indexOf(nodeId), 1); - } - if (editor && _diagram !== -1) { - await diagramRef.update({ nodes: [...new Set(_visibleNodes)] }); - } - setVisibleNodes([...new Set(_visibleNodes)]); - setLoadData(true); - }; - const handleExplanationChanges = e => { - setExplanation(e.currentTarget.value); - }; - const handleLabelChange = e => { - setLabel(e.currentTarget.value); - }; - - const showAllNodes = async () => { - let _visibleNodes = visibleNodes; - const _listOfDiagrams = [...listOfDiagrams]; - const _diagram = _listOfDiagrams.findIndex(diagram => diagram.id === selectedDiagram.id); - const diagramRef = firebase.db.collection("collabModelDiagrams").doc(listOfDiagrams[_diagram].id); - if (showAll) { - _visibleNodes = []; - setShowAll(false); - } else { - for (let node of allNodes) { - if (!_visibleNodes.includes(node.id)) { - _visibleNodes.push(node.id); - } - } - setShowAll(true); - } - _listOfDiagrams[_diagram].nodes = _visibleNodes; - if (editor && _diagram !== -1) { - await diagramRef.update({ nodes: [...new Set(_visibleNodes)] }); - } - setVisibleNodes(_visibleNodes); - setLoadData(true); - setStepLink(0); - setZoomState(null); - }; - - const handleInputValidation = event => { - const value = event.target.value; - const cleanedValue = value.replace(/[^0-9.]+/g, ""); - event.target.value = cleanedValue; - setLinkOrder(parseInt(cleanedValue)); - }; - - const nextLink = () => { - const _visibleNodes = []; - allNodes.forEach(node => { - node.children.forEach(child => { - if (stepLink + 1 >= child.order && child.order !== 0) { - if (!_visibleNodes.includes(child.id)) { - _visibleNodes.push(child.id); - } - if (!_visibleNodes.includes(node.id)) { - _visibleNodes.push(node.id); - } - } - }); - }); - setStepLink(prevActiveStep => (prevActiveStep + 1 < maxDepth ? prevActiveStep + 1 : maxDepth)); - setVisibleNodes(_visibleNodes); - setLoadData(true); - setShowAll(false); - setIngnorOrder(false); - }; - const previousLink = () => { - const _visibleNodes = []; - allNodes.forEach(node => { - node.children.forEach(child => { - if (stepLink - 1 >= child.order && child.order !== 0) { - if (!_visibleNodes.includes(child.id)) { - _visibleNodes.push(child.id); - } - if (!_visibleNodes.includes(node.id)) { - _visibleNodes.push(node.id); - } - } + const linkRef = firebase.db.collection("links").doc(selectedLink.id); + linkRef.update({ + certainty: selectedLink.certainty, + detail: selectedLink.detail, + polarity: selectedLink.polarity }); - }); - setVisibleNodes(_visibleNodes); - setStepLink(prevActiveStep => (prevActiveStep - 1 > 0 ? prevActiveStep - 1 : 0)); - setLoadData(true); - setShowAll(false); - setIngnorOrder(false); - }; - const deleteLink = async () => { - await firebase.db.runTransaction(async t => { - const nodeId = selectedLink.v; - const childId = selectedLink.w; - const nodeRef = firebase.firestore().collection("collabModelNodes").doc(nodeId); - const nodeDoc = await nodeRef.get(); - const nodeData = nodeDoc.data(); - const childIdx = nodeData.children.findIndex(child => child.id === childId); - const childp = nodeData.children[childIdx]; - for (let node of allNodes) { - let _children = node.children; - for (let _child of _children) { - if (_child.order > childp.order) { - _child.order = parseInt(_child.order) - 1; - } - if (nodeId === node.id && _child.id === childId) { - _child.order = 0; - _child.deleted = true; - } - } - const nodeRef = firebase.firestore().collection("collabModelNodes").doc(node.id); - t.update(nodeRef, { children: _children }); - } + setSelectedLink(null); }); - - setDeleteDialogLinkOpen(false); - setOpenModifyLink(false); - setExplanation(""); - setTypeLink(""); - setSelectedLink({}); - setLinkOrder(null); - setLoadData(true); }; const addChild = child => { - const _childIds = childrenIds; - if (_childIds.includes(child)) return; - _childIds.push(child); - setChildrenIds(_childIds); - setLoadData(true); + setNewNode(prev => { + const _prev = { ...prev }; + if (!_prev.children.includes(child)) { + _prev.children.push(child); + } + return _prev; + }); }; const removeChild = child => { - const _childIds = childrenIds; - if (!_childIds.includes(child)) return; - _childIds.splice(_childIds.indexOf(child), 1); - setChildrenIds(_childIds); - setLoadData(true); - }; - const resetOrder = () => { - const _visibleNodes = []; - allNodes.forEach(node => { - node.children.forEach(child => { - if (1 >= child.order && child.order !== 0) { - if (!_visibleNodes.includes(child.id)) { - _visibleNodes.push(child.id); - } - if (!_visibleNodes.includes(node.id)) { - _visibleNodes.push(node.id); - } - } - }); + setNewNode(prev => { + const _prev = { ...prev }; + if (_prev.children.includes(child)) { + _prev.children.splice(_prev.children.indexOf(child), 1); + } + return _prev; }); - setStepLink(1); - setVisibleNodes(_visibleNodes); - setLoadData(true); - setShowAll(false); - setIngnorOrder(false); }; + const handleOpenSidBar = () => { setOpenSideBar(old => !old); setZoomState(null); }; - const handleLegend = () => { - setOpenLegend(old => !old); - }; - const handlChangeDiagram = event => { - const _diagram = listOfDiagrams.find(diagram => diagram.name === event.target.value); + + const handleChangeDiagram = event => { + const _diagram = diagrams.find(diagram => diagram.title === event.target.value); + console.log("_diagram ==>", _diagram, event.target.value); setSelectedDiagram(_diagram); - setVisibleNodes(_diagram.nodes); - setShowAll(false); - setNodesLoded(false); + setGroups([]); + setNodes([]); + setLinks([]); + setSelectedGroups(new Set()); }; - const handleEditDiagram = async () => { - try { - setEditingDiagram(true); - const _listOfDiagrams = [...listOfDiagrams]; - const index = _listOfDiagrams.findIndex(d => d.id === selectedDiagram.id); - _listOfDiagrams[index].name = newDiagramName; - const diagramRef = firebase.db.collection("collabModelDiagrams").doc(selectedDiagram.id); - await diagramRef.update({ name: newDiagramName }); - setListOfDiagrams(_listOfDiagrams); - setSelectedDiagram(_listOfDiagrams[index]); - setOpenEditModal(false); - setEditingDiagram(false); - setNewDiagramName(""); - } catch (error) {} - }; - const handleCloseEditModal = () => { - setOpenEditModal(false); - }; + const downloadSvg = () => { + const svgElement = svgRef.current; + if (svgElement) { + const clone = svgElement.cloneNode(true); + const svgStyles = document.createElement("style"); + const cssRules = Array.from(document.styleSheets) + .reduce((acc, styleSheet) => { + try { + return acc.concat(Array.from(styleSheet.cssRules)); + } catch { + return acc; // Ignore CSS rules we can't access + } + }, []) + .map(rule => rule.cssText) + .join(" "); - const handlNewDiagramName = event => { - setNewDiagramName(event.target.value); - }; - const editDiagram = () => { - setNewDiagramName(selectedDiagram.name); - setOpenEditModal(true); - }; + svgStyles.textContent = cssRules; + clone.insertBefore(svgStyles, clone.firstChild); - const addNewDiagram = () => { - setNewDiagramName(""); - setOpenAddModal(true); + const svgData = new XMLSerializer().serializeToString(clone); + const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" }); + const url = URL.createObjectURL(svgBlob); + + const downloadLink = document.createElement("a"); + downloadLink.href = url; + downloadLink.download = "graph.svg"; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + + URL.revokeObjectURL(url); + } }; - const handleCloseAddModal = () => setOpenAddModal(false); - - const handleAddDiagram = () => { - if (listOfDiagrams.findIndex(d => d.name === newDiagramName) === -1) { - const ref = firebase.db.collection("collabModelDiagrams").doc(); - ref.set({ - name: newDiagramName, - nodes: [] + const generateNewDiagram = async () => { + try { + const documentDetailed = await promptItDiagram("Generate a new diagram:", "Generate", "Cancel"); + + console.log(documentDetailed); + if (!documentDetailed.trim()) return; + setLoadingResponse(true); + + const r = await axios.post("/api/generateCollabDiagram", { documentDetailed }); + console.log(r.data.response, "generateCollabDiagram"); + setLoadingResponse(true); + + const response = r.data.response; + console.log("response ==>", response); + + const newDiagramRef = firebase.db.collection("diagrams").doc(); + + for (let group of response.groupHierarchy) { + const groupRef = firebase.db.collection("groups").doc(); + const id = groupRef.id; + group.id = id; + groupRef.set({ + id, + createdAt: new Date(), + ...group, + diagrams: [newDiagramRef.id] + }); + } + for (let node of response["nodes"]) { + const nodeRef = firebase.db.collection("nodes").doc(); + const id = nodeRef.id; + const _groups = node.groups.map(c => { + return { + id: response.groupHierarchy.find(g => g.label === c).id, + label: response.groupHierarchy.find(g => g.label === c).label + }; + }); + node.groups = _groups; + node.id = id; + const _node = { + ...node, + createdAt: new Date() + }; + console.log("_node ==>", _node); + nodeRef.set({ + ..._node, + diagrams: [newDiagramRef.id] + }); + } + for (let link of response["links"]) { + link.source = response["nodes"].find(c => c.label === link.source)?.id || ""; + link.target = response["nodes"].find(c => c.label === link.target)?.id || ""; + const linkRef = firebase.db.collection("links").doc(); + linkRef.set({ ...link, diagrams: [newDiagramRef.id] }); + } + console.log(response); + setLoadingResponse(false); + const diagramName = await promptIt("What do you want to call the diagram?", "Ok"); + newDiagramRef.set({ + title: diagramName, + id: newDiagramRef.id }); - const _listOfDiagrams = [...listOfDiagrams]; - _listOfDiagrams.push({ id: ref.id, name: newDiagramName, nodes: [] }); - setListOfDiagrams(_listOfDiagrams); + } catch (error) { + console.error(error); + } finally { + setLoadingResponse(false); + } + }; + const improveDiagram = async () => { + console.log("first"); + const response = await promptItDiagram("Improve the diagram:", "improve", "cancel"); + /* const documentDetailed = await promptIt("Enter the prompt bellow", "Start"); + console.log("documentDetailed ==>", documentDetailed); */ + }; + console.log("selectedLink ==>", selectedLink); + + const deleteLink = async () => { + if (await confirmIt("Are you sure you want to delete the link?", "Delete", "Keep")) { + if (selectedLink) { + const linkRef = firebase.db.collection("links").doc(selectedLink.id); + linkRef.delete(); + setSelectedLink(null); + } } - setNewDiagramName(""); - setOpenAddModal(false); }; + console.log("newNode.groups", newNode, groups); + return ( - - - Update the diagram name: - - - - - - - - - - - Add new diagram: - - - - - - - - - - - {" "} - - - - {" "} - - Choose nodes to show their causal relations. - {" "} - Show All the Nodes - - - {allNodes.map((node, index) => ( - + {diagrams.length > 0 ? ( + + + + - { - handleVisibileNodes(node); - }} - /> - - - ))} - - - - - - {Object.values(nodeTypes).map((resource, index) => ( - + {Object.keys(nodes).length > 0 && } + {!openSideBar && ( + + + + )} - {resource.type} - - - ))} - {[ - { text: "Known Positive Effect", color: "#56E41B" }, - { text: "Hypothetical Positive Effect", color: "#1BBAE4" }, - { text: "Known Negative Effect", color: "#A91BE4" }, - { text: "Hypothetical Negative Effect", color: "#E4451B" } - ].map((resource, index) => ( - - - - {resource.text} - - - ))} - - - - - - - - - - - - - - - - - - - - - {visibleNodes.length > 0 ? ( - - ) : ( -
- - To show the nodes in the diagram, check them in the list. - -
- )} - {!openSideBar && ( - - )} - - {!openLegend && isMobile && ( - - )} - {visibleNodes.length > 0 && !isMobile && ( - - {Object.values(nodeTypes).map((resource, index) => ( - - ))} - {editor && ( - + {" "} + + {loadingResponse ? ( + + + + ) : ( + + + + )} + + {/* + {loadingResponse ? ( + { - setIsModalAddTypeOpen(true); + display: "flex", + justifyContent: "center" }} + > + + + ) : ( + - + )} - {[ - { text: "Known Positive Effect", color: "#56E41B" }, - { text: "Hypothetical Positive Effect", color: "#1BBAE4" }, - { text: "Known Negative Effect", color: "#A91BE4" }, - { text: "Hypothetical Negative Effect", color: "#E4451B" } - ].map((resource, index) => ( - - - - {resource.text} - - - ))} - - )} -
- - {openModifyLink && editor && ( - - - - - :not(style)": { m: 0.5, width: "25ch" } - }} - noValidate - autoComplete="off" - > - - Type - - - {/* */} + + {loadingResponse ? ( + */} - - - - + > + + + ) : ( { - setDeleteDialogLinkOpen(true); + variant="contained" + onClick={AddNewNode} + sx={{ width: "35px", height: "35px", border: "1px solid gray", borderRadius: "10px" }} + disabled={newNode?.new} + > + + + )} + + + {diagrams.length > 0 && ( + + Diagram + + + )} + + + + - + - - )} - {openModifyLink && !editor && explanationLink !== "" && ( - {explanationLink} - )} - {openAddNode && ( - - { - setTitle(e.currentTarget.value); - }} - /> - - :not(style)": { m: 0.5, width: "25ch" } - }} - noValidate - autoComplete="off" - > - - Type - - - - children - - + + Type + + + + children + + + + Groups + + + + + + + + {!newNode?.new && ( + + )} + + - - - {" "} + - { - setDeleteDialogOpen(true); + setSelectedLink(null); }} + autoFocus + sx={{ borderRadius: "25px" }} > - - + Cancel + - - - )} - + + )} - - - - } - label={darkMode ? "Dark Mode" : "Light Mode"} - sx={{ mb: "15px", color: darkMode ? "white" : "black" }} - /> - {editor && ( - - )} - {editor && ( - - )} - {editor && Object.keys(selectedDiagram).length > 0 && !openModifyLink && !openAddNode && ( - + {Object.keys(nodes).length > 0 && !isMobile && ( + + {Object.values(nodeTypes).map((resource, index) => ( + + ))} + {editor && ( + + { + setIsModalAddTypeOpen(true); + }} + variant="contained" + > + + + + )} + + {Object.entries(LINKS_TYPES).map(resource => ( + + + {resource[0]} + + ))} + + )} - - {!openModifyLink && - !openAddNode && - Object.keys(selectedDiagram).length > 0 && - listOfDiagrams.length > 0 && ( - - diagram - - - )} -
- - - {openSideBar && ( - - {openSideBar && !isMobile && ( - + + {openSideBar && ( + + - )} - - - {" "} - - Choose nodes to show their causal relations. - {" "} - Show All the Nodes - - - {allNodes.map((node, index) => ( - + - { - handleVisibileNodes(node); - }} + + + Choose groups to show their causal relations: + + {openSideBar && !isMobile && ( + + + + )} + + + + - - - ))} - - - )} + {/* */} +
+ + + )} + - - + + ) : ( + + + Generate a diagram + + + )} {editor && ( { editNodeType={editNodeType} /> )} + {ConfirmDialog} + {PromptDialog} ); }; diff --git a/src/hooks/useConfirmDialog.js b/src/hooks/useConfirmDialog.js new file mode 100644 index 000000000..cd7450fc6 --- /dev/null +++ b/src/hooks/useConfirmDialog.js @@ -0,0 +1,100 @@ +import { Button, Dialog, DialogActions, DialogContent, DialogContentText, TextField } from "@mui/material"; +import React, { useCallback, useState } from "react"; + +const useDialog = () => { + const [isOpen, setIsOpen] = useState(false); + const [dialogMessage, setDialogMessage] = useState(""); + const [isPrompt, setIsPrompt] = useState(false); + const [inputValue, setInputValue] = useState(""); + const resolveRef = React.useRef(null); + const [confirmation, setConfirmation] = useState(""); + const [cancel, setCancel] = useState(""); + + const showDialog = useCallback((message, prompt = false, confirmation, cancel) => { + setDialogMessage(message); + setIsOpen(true); + setIsPrompt(prompt); + setConfirmation(confirmation); + setCancel(cancel); + + return new Promise(resolve => { + resolveRef.current = resolve; + }); + }, []); + + const closeDialog = useCallback( + confirmed => { + setIsOpen(false); + setDialogMessage(""); + setInputValue(""); + + if (resolveRef.current) { + resolveRef.current(isPrompt ? inputValue : confirmed); + } + }, + [isPrompt, inputValue] + ); + + const handleInputChange = event => { + setInputValue(event.target.value); + }; + + const ConfirmDialog = ( + closeDialog(false)} sx={{ width: "100%" }}> + + {dialogMessage} + {isPrompt && ( + + )} + + + + {!isPrompt && cancel && ( + + )} + + + ); + + const promptIt = useCallback( + (message, confirmation, cancel) => showDialog(message, true, confirmation, cancel), + [showDialog] + ); + const confirmIt = useCallback( + (message, confirmation, cancel) => showDialog(message, false, confirmation, cancel), + [showDialog] + ); + + return { promptIt, confirmIt, ConfirmDialog }; +}; + +export default useDialog; diff --git a/src/hooks/usePromptDialog.js b/src/hooks/usePromptDialog.js new file mode 100644 index 000000000..11ad54d8f --- /dev/null +++ b/src/hooks/usePromptDialog.js @@ -0,0 +1,150 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + TextField, + Grid, + Typography +} from "@mui/material"; +import React, { useCallback, useEffect, useState } from "react"; +import { useRecoilValue } from "recoil"; +import { firebaseState } from "../store/AuthAtoms"; +import Box from "@mui/material/Box"; + +const useDialog = () => { + const [isOpen, setIsOpen] = useState(false); + const [dialogMessage, setDialogMessage] = useState(""); + const [inputValue, setInputValue] = useState(""); + const resolveRef = React.useRef(null); + const [confirmation, setConfirmation] = useState(""); + const [cancel, setCancel] = useState(""); + const firebase = useRecoilValue(firebaseState); + const [llmPrompt, setLlmPrompt] = useState(""); + + const getPrompt = async type => { + const promptDoc = await firebase.db.collection("diagramPrompts").doc(type).get(); + const promptData = promptDoc.data(); + setLlmPrompt(promptData?.prompt || ""); + }; + + const showDialog = useCallback((message, confirmation, cancel) => { + getPrompt(confirmation.toLowerCase()); + setDialogMessage(message); + setIsOpen(true); + setConfirmation(confirmation); + setCancel(cancel); + + return new Promise(resolve => { + resolveRef.current = resolve; + }); + }, []); + + const closeDialog = useCallback(() => { + setIsOpen(false); + setDialogMessage(""); + setInputValue(""); + setLlmPrompt(""); + + if (resolveRef.current) { + savePrompt(); + resolveRef.current(inputValue || ""); + } + }, [inputValue, llmPrompt]); + + const handleUserInputChange = event => { + setInputValue(event.target.value); + }; + + const handleLlmPromptChange = event => { + setLlmPrompt(event.target.value); + }; + const savePrompt = () => { + const promptRef = firebase.db.collection("diagramPrompts").doc(confirmation.toLowerCase()); + promptRef.set({ + prompt: llmPrompt + }); + }; + + const PromptDialog = ( + + + {dialogMessage} + + + + {confirmation.toLowerCase() === "generate" ? "Enter your document bellow:" : "Your input:"} + + + + + System Prompt: + + + + + + + {cancel && ( + + )} + + + ); + + const promptItDiagram = useCallback( + (message, confirmation, cancel, type) => showDialog(message, confirmation, cancel), + [showDialog] + ); + + return { promptItDiagram, PromptDialog }; +}; + +export default useDialog;