├── public ├── upload.js ├── utils.js ├── toast-container-init.js ├── custom-styles.css ├── development-tasks.js ├── clipboard-operations.js ├── toast.js ├── database-selection.js ├── upload-handler.js ├── clipboard-utils.js ├── index.html ├── toast-notifications.js ├── shared.js ├── development-tasks-display.js └── development-steps.js ├── .env ├── .gitignore ├── package.json ├── handlers └── openaiMessageHandler.js ├── routes ├── developmentPlans.js └── developmentSteps.js ├── openai-handler.js ├── openai-service.js ├── README.md └── app.js /public/upload.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | OPENAI_MODEL=gpt-4-turbo-preview -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pythagora-io/gpt-pilot-db-analysis-tool/HEAD/.gitignore -------------------------------------------------------------------------------- /public/utils.js: -------------------------------------------------------------------------------- 1 | export function insertAfter(newNode, referenceNode) { 2 | referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "body-parser": "^1.20.2", 4 | "bootstrap": "^5.3.2", 5 | "cors": "^2.8.5", 6 | "dotenv": "^16.4.1", 7 | "express": "^4.18.2", 8 | "multer": "^1.4.5-lts.1", 9 | "node-fetch": "^2.7.0", 10 | "sqlite3": "^5.1.7" 11 | }, 12 | "scripts": { 13 | "start": "node app.js" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /public/toast-container-init.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', (event) => { 2 | const toastContainer = document.createElement('div'); 3 | toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3'; 4 | toastContainer.id = 'toastContainer'; 5 | document.body.appendChild(toastContainer); 6 | console.log('Toast container initialized.'); 7 | }); 8 | 9 | export {}; -------------------------------------------------------------------------------- /public/custom-styles.css: -------------------------------------------------------------------------------- 1 | .list-group-item.active { 2 | background-color: #007bff; 3 | color: white; 4 | border-color: #007bff; 5 | } 6 | 7 | .list-group-item.active:hover { 8 | background-color: #0069d9; 9 | border-color: #0062cc; 10 | } 11 | 12 | .step-number { 13 | font-weight: bold; 14 | margin-right: 5px; 15 | display: inline-block; 16 | min-width: 50px; /* Ensures consistent width for all step numbers */ 17 | } 18 | 19 | @media(max-width: 768px) { 20 | .step-number { 21 | font-size: 0.95em; 22 | } 23 | } 24 | 25 | @media(max-width: 576px) { 26 | .step-number { 27 | font-size: 0.9em; 28 | } 29 | } -------------------------------------------------------------------------------- /public/development-tasks.js: -------------------------------------------------------------------------------- 1 | import { displayDevelopmentTasks } from './development-tasks-display.js'; 2 | 3 | function fetchAndDisplayDevelopmentTasks(appId, dbName) { 4 | const xhr = new XMLHttpRequest(); 5 | xhr.open('GET', `/development_plans?app_id=${encodeURIComponent(appId)}&db=${encodeURIComponent(dbName)}`, true); 6 | xhr.onload = function() { 7 | if (xhr.status === 200) { 8 | const developmentTasks = JSON.parse(xhr.responseText); 9 | const tasks = developmentTasks.flat(); 10 | displayDevelopmentTasks(tasks, appId, dbName); 11 | } else { 12 | alert('Failed to load development tasks for the selected app'); 13 | } 14 | }; 15 | xhr.send(); 16 | } 17 | 18 | export { fetchAndDisplayDevelopmentTasks }; -------------------------------------------------------------------------------- /handlers/openaiMessageHandler.js: -------------------------------------------------------------------------------- 1 | const { callOpenAIAPIService } = require('../openai-service'); 2 | 3 | async function handleOpenAIMessage(req, res) { 4 | try { 5 | if (!Array.isArray(req.body.messages)) { 6 | throw new Error("Invalid request: 'messages' must be an array"); 7 | } 8 | 9 | const messages = req.body.messages.map(message => { 10 | if (typeof message !== 'object' || !message.role) { 11 | throw new Error("Invalid message format: Each 'message' object must have a 'role' property"); 12 | } 13 | return message; 14 | }); 15 | 16 | const openaiData = await callOpenAIAPIService(messages); 17 | 18 | res.json(openaiData); 19 | } catch (error) { 20 | if (error.message.includes("Invalid message format")) { 21 | return res.status(400).send(error.message); 22 | } 23 | 24 | console.error(`Error during OpenAI API call: ${error.message}`); 25 | res.status(503).send('Service unavailable. Could not reach the OpenAI GPT-4 API.'); 26 | } 27 | } 28 | 29 | module.exports = { handleOpenAIMessage }; -------------------------------------------------------------------------------- /public/clipboard-operations.js: -------------------------------------------------------------------------------- 1 | import { showToast } from './toast-notifications.js'; 2 | 3 | export function copyToClipboard(text) { 4 | if (navigator.clipboard && window.isSecureContext) { 5 | navigator.clipboard.writeText(text).then(() => { 6 | showToast('Copied to clipboard!', 'success'); 7 | console.log('Content copied to clipboard.'); 8 | }).catch((err) => { 9 | showToast('Failed to copy to clipboard!', 'danger'); 10 | console.error('Failed to copy using Clipboard API:', err); 11 | }); 12 | } else { 13 | const textArea = document.createElement('textarea'); 14 | textArea.value = text; 15 | document.body.appendChild(textArea); 16 | textArea.focus(); 17 | textArea.select(); 18 | try { 19 | const successful = document.execCommand('copy'); 20 | showToast(successful ? 'Copied to clipboard!' : 'Copy failed!', successful ? 'success' : 'danger'); 21 | console.log(successful ? 'Copied content using execCommand.' : 'Copy using execCommand failed.'); 22 | } catch (err) { 23 | showToast('Failed to copy to clipboard!', 'danger'); 24 | console.error('Fallback for Clipboard API failed:', err); 25 | } finally { 26 | document.body.removeChild(textArea); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /routes/developmentPlans.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const sqlite3 = require('sqlite3').verbose(); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | const router = express.Router(); 7 | 8 | router.get('/development_plans', (req, res) => { 9 | const appId = req.query.app_id; 10 | if (!appId) { 11 | return res.status(400).send('App ID is required'); 12 | } 13 | 14 | const dbName = req.query.db; 15 | if (!dbName) { 16 | return res.status(400).send('Database name is required'); 17 | } 18 | 19 | const dbPath = path.join('uploads', `${dbName}.sqlite`); 20 | if (!fs.existsSync(dbPath)) { 21 | return res.status(404).send('Database file not found'); 22 | } 23 | 24 | let db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => { 25 | if (err) { 26 | res.status(500).send('Error opening database: ' + err.message); 27 | return; 28 | } 29 | }); 30 | 31 | const sql = 'SELECT development_plan FROM development_planning WHERE app_id = ?'; 32 | 33 | db.all(sql, [appId], (err, rows) => { 34 | if (err) { 35 | res.status(500).send('Error reading from database: ' + err.message); 36 | return; 37 | } 38 | 39 | const plans = rows.map(row => JSON.parse(row.development_plan)); 40 | res.json(plans); 41 | }); 42 | 43 | db.close((err) => { 44 | if (err) { 45 | console.error('Error closing database: ' + err.message); 46 | } 47 | }); 48 | }); 49 | 50 | module.exports = router; -------------------------------------------------------------------------------- /openai-handler.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const fetch = require('node-fetch'); 3 | 4 | async function postToOpenAIAPI(messages) { 5 | const endpoint = process.env.OPENAI_MODEL === 'gpt-4-turbo-preview' 6 | ? 'https://api.openai.com/v1/chat/completions' 7 | : 'https://api.openai.com/v1/completions'; 8 | 9 | const headers = { 10 | 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, 11 | 'Content-Type': 'application/json' 12 | }; 13 | 14 | let payload = {}; 15 | if (process.env.OPENAI_MODEL === 'gpt-4-turbo-preview') { 16 | payload = { 17 | model: 'gpt-4-turbo-preview', 18 | messages: messages.map(m => { 19 | return {role: m.role || 'user', content: m.content}; 20 | }) 21 | }; 22 | } else { 23 | payload = { 24 | prompt: messages.map(message => message.content).join("\n"), 25 | max_tokens: 150 26 | }; 27 | } 28 | 29 | try { 30 | const response = await fetch(endpoint, { 31 | method: 'POST', 32 | headers: headers, 33 | body: JSON.stringify(payload) 34 | }); 35 | if (!response.ok) { 36 | const errorBody = await response.text(); 37 | throw new Error(`API request failed with status ${response.status}: ${errorBody}`); 38 | } 39 | return await response.json(); 40 | } catch (error) { 41 | console.error(`OpenAI API call error: ${error.message}`); 42 | throw new Error(`Error while calling OpenAI API: ${error.message}`); 43 | } 44 | } 45 | 46 | module.exports = { postToOpenAIAPI }; -------------------------------------------------------------------------------- /public/toast.js: -------------------------------------------------------------------------------- 1 | export function showBootstrapToast(message, type) { 2 | const toastContainer = document.getElementById('toastContainer') || createToastContainer(); 3 | const toast = createToast(message, type); 4 | toastContainer.appendChild(toast); 5 | const toastInstance = new bootstrap.Toast(toast); 6 | toastInstance.show(); 7 | } 8 | 9 | function createToast(message, type) { 10 | const toast = document.createElement('div'); 11 | toast.className = `toast align-items-center text-white bg-${type} border-0`; 12 | toast.role = 'alert'; 13 | toast.ariaLive = 'assertive'; 14 | toast.ariaAtomic = 'true'; 15 | const toastBody = document.createElement('div'); 16 | toastBody.className = 'd-flex'; 17 | const toastMessage = document.createElement('div'); 18 | toastMessage.className = 'toast-body'; 19 | toastMessage.textContent = message; 20 | const toastButton = document.createElement('button'); 21 | toastButton.type = 'button'; 22 | toastButton.className = 'btn-close me-2 m-auto'; 23 | toastButton.dataset.bsDismiss = 'toast'; 24 | toastButton.ariaLabel = 'Close'; 25 | toastBody.appendChild(toastMessage); 26 | toastBody.appendChild(toastButton); 27 | toast.appendChild(toastBody); 28 | return toast; 29 | } 30 | 31 | function createToastContainer() { 32 | const container = document.createElement('div'); 33 | container.id = 'toastContainer'; 34 | container.className = 'toast-container position-fixed bottom-0 end-0 p-3'; 35 | container.style.zIndex = '11'; 36 | document.body.appendChild(container); 37 | return container; 38 | } 39 | -------------------------------------------------------------------------------- /public/database-selection.js: -------------------------------------------------------------------------------- 1 | import { fetchAndDisplayApps } from './shared.js'; 2 | import { initializeUploadForm } from './upload-handler.js'; 3 | 4 | const existingDbsSelect = document.getElementById('existingDbs'); 5 | 6 | function fetchDatabasesAndPopulateDropdown() { 7 | console.log('fetchDatabasesAndPopulateDropdown called'); // New line added for debugging 8 | const xhr = new XMLHttpRequest(); 9 | xhr.open('GET', '/databases', true); 10 | xhr.onload = function() { 11 | if (xhr.status === 200) { 12 | const databases = JSON.parse(xhr.responseText); 13 | while(existingDbsSelect.options.length > 1) { 14 | existingDbsSelect.remove(1); 15 | } 16 | databases.forEach(function(database) { 17 | const option = document.createElement('option'); 18 | const dbNameWithoutExtension = database.name.replace(/\.(sqlite|db)$/i, ''); 19 | option.textContent = dbNameWithoutExtension; 20 | option.value = dbNameWithoutExtension; 21 | existingDbsSelect.appendChild(option); 22 | }); 23 | } else { 24 | alert('Failed to load databases'); 25 | } 26 | }; 27 | xhr.send(); 28 | } 29 | 30 | document.addEventListener('DOMContentLoaded', function() { 31 | fetchDatabasesAndPopulateDropdown(); 32 | initializeUploadForm(fetchAndDisplayApps, fetchDatabasesAndPopulateDropdown); 33 | }); 34 | 35 | existingDbsSelect.addEventListener('change', function() { 36 | if (existingDbsSelect.value) { 37 | fetchAndDisplayApps(existingDbsSelect.value); 38 | } 39 | }); 40 | 41 | export { fetchDatabasesAndPopulateDropdown }; -------------------------------------------------------------------------------- /public/upload-handler.js: -------------------------------------------------------------------------------- 1 | export function initializeUploadForm(fetchAndDisplayApps, fetchDatabasesAndPopulateDropdown) { 2 | const form = document.getElementById('uploadForm'); 3 | let selectedDatabaseName = ""; 4 | 5 | form.addEventListener('submit', function(event) { 6 | event.preventDefault(); 7 | 8 | const dbNameInput = document.getElementById('dbName'); 9 | const dbFileInput = document.getElementById('dbFile'); 10 | const dbName = dbNameInput.value.trim(); 11 | const dbFile = dbFileInput.files[0]; 12 | 13 | if (!dbName) { 14 | alert('Please enter a name for the database'); 15 | return; 16 | } 17 | 18 | if (!dbFile || !(/\.(sqlite|db)$/i).test(dbFile.name)) { 19 | alert('Please upload a valid SQLite database file (.sqlite or .db)'); 20 | return; 21 | } 22 | 23 | const formData = new FormData(); 24 | formData.append('dbname', dbName); 25 | formData.append('dbfile', dbFile); 26 | 27 | const xhr = new XMLHttpRequest(); 28 | xhr.open('POST', '/upload', true); 29 | xhr.onreadystatechange = function() { 30 | if (xhr.readyState === XMLHttpRequest.DONE) { 31 | const responseMessage = xhr.responseText; 32 | if (xhr.status === 200) { 33 | alert('Database uploaded successfully'); 34 | selectedDatabaseName = dbName; // Set the current database name 35 | fetchAndDisplayApps(dbName); // Update the displayed apps 36 | fetchDatabasesAndPopulateDropdown(); // Update the dropdown list 37 | } else { 38 | alert(`An error occurred during the upload: ${responseMessage}`); 39 | } 40 | } 41 | }; 42 | xhr.send(formData); 43 | }); 44 | } -------------------------------------------------------------------------------- /public/clipboard-utils.js: -------------------------------------------------------------------------------- 1 | import { showToast } from './toast-notifications.js'; 2 | 3 | export function copyToClipboard(stepData) { 4 | // Create the data structure for the step data 5 | const dataToCopy = { 6 | prompt_path: stepData.prompt_path, 7 | messages: stepData.messages, 8 | llm_response: stepData.llm_response, 9 | prompt_data: stepData.prompt_data 10 | }; 11 | 12 | // Stringify the data to copy 13 | const jsonString = JSON.stringify(dataToCopy, null, 2); 14 | 15 | // Try to use the Clipboard API if available 16 | if (navigator.clipboard && window.isSecureContext) { 17 | navigator.clipboard.writeText(jsonString).then(() => { 18 | showToast('Data copied to clipboard!', 'success'); 19 | console.log('Data successfully copied to clipboard.'); 20 | }).catch((err) => { 21 | console.error('Could not copy text to clipboard: ', err); 22 | showToast('Copy failed, please try manually.', 'danger'); 23 | console.error('Clipboard API error with trace:', err); 24 | }); 25 | } else { 26 | // Fallback to textarea method for browsers without Clipboard API support 27 | const textArea = document.createElement('textarea'); 28 | textArea.value = jsonString; 29 | document.body.appendChild(textArea); 30 | textArea.select(); 31 | try { 32 | const successful = document.execCommand('copy'); 33 | showToast(successful ? 'Data copied to clipboard!' : 'Copy failed, please try manually.', successful ? 'success' : 'danger'); 34 | console.log(successful ? 'Data successfully copied to clipboard using document.execCommand.' : 'Failed to copy data using document.execCommand.'); 35 | } catch (err) { 36 | console.error('Fallback copy text error: ', err); 37 | showToast('Copy failed, please try manually.', 'danger'); 38 | console.error('execCommand Copy Error with trace:', err); 39 | } finally { 40 | document.body.removeChild(textArea); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /openai-service.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | async function callOpenAIAPIService(messages) { 4 | const openaiApiKey = process.env.OPENAI_API_KEY; 5 | 6 | if (!openaiApiKey) { 7 | throw new Error('OpenAI API key is not defined in the environment variables.'); 8 | } 9 | 10 | // Using a variable endpoint based on the model to support chat models 11 | const chatEndpoint = 'https://api.openai.com/v1/chat/completions'; 12 | const completionsEndpoint = 'https://api.openai.com/v1/completions'; 13 | 14 | let endpoint; 15 | const headers = { 16 | 'Content-Type': 'application/json', 17 | 'Authorization': `Bearer ${openaiApiKey}` 18 | }; 19 | 20 | let payload; 21 | // GPT-4 Turbo uses the chat API 22 | if(process.env.OPENAI_MODEL === 'gpt-4-turbo-preview') { 23 | endpoint = chatEndpoint; 24 | // Constructing the payload for the chat API 25 | payload = { 26 | model: 'gpt-4-turbo-preview', 27 | messages: messages.map(m => ({ role: m.role, content: m.content })) 28 | }; 29 | } else { 30 | // Default to completions endpoint for other models 31 | endpoint = completionsEndpoint; 32 | // Constructing the payload for the completions API 33 | payload = { 34 | model: process.env.OPENAI_MODEL || 'gpt-3.5-openai', 35 | prompt: messages.map(m => m.content).join('\n'), 36 | max_tokens: 150, 37 | temperature: 0.5 38 | }; 39 | } 40 | 41 | try { 42 | const response = await fetch(endpoint, { 43 | method: 'POST', 44 | headers: headers, 45 | body: JSON.stringify(payload), 46 | }); 47 | 48 | if (!response.ok) { 49 | const responseText = await response.text(); 50 | throw new Error(`OpenAI API error: ${response.statusText} (HTTP status: ${response.status}) Details: ${responseText}`); 51 | } 52 | 53 | return await response.json(); 54 | } catch (error) { 55 | console.error(`Error while calling OpenAI API service: ${error.message}`); 56 | throw new Error(`OpenAI API service error: ${error.message}`); 57 | } 58 | } 59 | 60 | module.exports = { callOpenAIAPIService }; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SQLite DB Analysis Tool 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 | 22 | 26 |
27 | 28 |
29 |
30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 | 41 |
42 |
43 |
44 | 45 |
46 | 47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /public/toast-notifications.js: -------------------------------------------------------------------------------- 1 | export function showToast(message, type) { 2 | try { 3 | let toastContainer = document.querySelector('.toast-container'); 4 | if (!toastContainer) { 5 | toastContainer = createToastContainer(); 6 | document.body.appendChild(toastContainer); 7 | } 8 | 9 | let existingToast = toastContainer.querySelector('.toast.show'); 10 | if (existingToast) { 11 | console.log('Hiding existing toast message.'); 12 | const existingToastInstance = bootstrap.Toast.getInstance(existingToast); 13 | existingToastInstance.hide(); 14 | } 15 | 16 | const toast = createToast(message, type); 17 | toastContainer.appendChild(toast); 18 | console.log('Displaying new toast message.'); 19 | const toastInstance = new bootstrap.Toast(toast); 20 | toastInstance.show(); 21 | 22 | toast.addEventListener('hidden.bs.toast', () => { 23 | toastContainer.removeChild(toast); 24 | console.log('Toast message closed.'); 25 | }); 26 | } catch (err) { 27 | console.error('Error showing toast notification:', err.stack || err); 28 | } 29 | } 30 | 31 | function createToast(message, type) { 32 | const toast = document.createElement('div'); 33 | toast.className = `toast align-items-center text-white bg-${type} border-0`; 34 | toast.role = 'alert'; 35 | toast.ariaLive = 'assertive'; 36 | toast.ariaAtomic = 'true'; 37 | 38 | const toastBody = document.createElement('div'); 39 | toastBody.className = 'd-flex'; 40 | 41 | const toastMessage = document.createElement('div'); 42 | toastMessage.className = 'toast-body'; 43 | toastMessage.textContent = message; 44 | 45 | const toastButton = document.createElement('button'); 46 | toastButton.type = 'button'; 47 | toastButton.className = 'btn-close me-2 m-auto'; 48 | toastButton.dataset.bsDismiss = 'toast'; 49 | toastButton.ariaLabel = 'Close'; 50 | 51 | toastBody.appendChild(toastMessage); 52 | toastBody.appendChild(toastButton); 53 | toast.appendChild(toastBody); 54 | 55 | return toast; 56 | } 57 | 58 | function createToastContainer() { 59 | const container = document.createElement('div'); 60 | container.id = 'toastContainer'; 61 | container.className = 'toast-container position-fixed bottom-0 end-0 p-3'; 62 | container.style.zIndex = '11'; // Ensure toasts are above modal overlays which usually have a z-index of 10. 63 | return container; 64 | } -------------------------------------------------------------------------------- /public/shared.js: -------------------------------------------------------------------------------- 1 | import { fetchAndDisplayDevelopmentTasks } from './development-tasks.js'; 2 | 3 | function removeElementById(elementId) { 4 | const existingElement = document.getElementById(elementId); 5 | if (existingElement) { 6 | existingElement.remove(); 7 | } 8 | } 9 | 10 | function clearDevelopmentViews() { 11 | removeElementById('tasksContainer'); 12 | removeElementById('tasksHeading'); 13 | removeElementById('stepsContainer'); 14 | removeElementById('stepsHeading'); 15 | } 16 | 17 | function clearActiveFromElements(elements) { 18 | elements.forEach(element => element.classList.remove('active')); 19 | } 20 | 21 | function fetchAndDisplayApps(databaseName) { 22 | clearDevelopmentViews(); 23 | removeElementById('appsList'); 24 | 25 | const xhr = new XMLHttpRequest(); 26 | xhr.open('GET', '/apps?db=' + encodeURIComponent(databaseName), true); 27 | xhr.onload = function() { 28 | if (xhr.status === 200) { 29 | const apps = JSON.parse(xhr.responseText); 30 | displayApps(apps, databaseName); 31 | } else { 32 | alert('Failed to load apps from the selected database'); 33 | } 34 | }; 35 | xhr.send(); 36 | } 37 | 38 | function displayApps(apps, databaseName) { 39 | const appsContainer = document.getElementById('appsContainer'); 40 | 41 | let appsHeading = document.getElementById('appsHeading'); 42 | if (!appsHeading) { 43 | appsHeading = document.createElement('h3'); 44 | appsHeading.id = 'appsHeading'; 45 | appsHeading.textContent = 'Apps'; 46 | appsHeading.classList.add('mb-3'); 47 | appsContainer.appendChild(appsHeading); 48 | } 49 | 50 | const appsList = document.createElement('ul'); 51 | appsList.id = 'appsList'; 52 | appsList.classList.add('list-group'); 53 | 54 | apps.forEach(function(app) { 55 | const appItem = document.createElement('li'); 56 | appItem.textContent = app.name; 57 | appItem.id = 'app_' + app.id; 58 | appItem.classList.add('list-group-item', 'app-item'); 59 | appItem.dataset.appId = app.id; 60 | appItem.addEventListener('click', function() { 61 | clearActiveFromElements(Array.from(document.getElementsByClassName('app-item'))); 62 | clearDevelopmentViews(); 63 | appItem.classList.add('active'); 64 | fetchAndDisplayDevelopmentTasks(app.id, databaseName); 65 | }); 66 | appsList.appendChild(appItem); 67 | }); 68 | appsContainer.appendChild(appsList); 69 | } 70 | 71 | export function highlightSelectedItem(itemSelector, activeClass) { 72 | const elements = document.querySelectorAll(itemSelector); 73 | clearActiveFromElements(elements); 74 | this.classList.add(activeClass); 75 | } 76 | 77 | export { fetchAndDisplayApps, fetchAndDisplayDevelopmentTasks }; -------------------------------------------------------------------------------- /public/development-tasks-display.js: -------------------------------------------------------------------------------- 1 | import { fetchAndDisplayDevelopmentSteps } from './development-steps.js'; 2 | import { highlightSelectedItem } from './shared.js'; 3 | 4 | export function displayDevelopmentTasks(developmentTasks, appId, dbName) { 5 | const tasksContainer = document.getElementById('tasksContainer') || document.createElement('ul'); 6 | tasksContainer.id = 'tasksContainer'; 7 | tasksContainer.classList.add('list-group', 'mt-3'); 8 | tasksContainer.innerHTML = ''; 9 | 10 | let tasksHeading = document.getElementById('tasksHeading'); 11 | if (!tasksHeading) { 12 | tasksHeading = document.createElement('h3'); 13 | tasksHeading.id = 'tasksHeading'; 14 | tasksHeading.classList.add('mb-3'); 15 | tasksHeading.textContent = 'Development Tasks'; 16 | tasksContainer.before(tasksHeading); 17 | } 18 | 19 | developmentTasks.forEach((task, index) => { 20 | const taskItem = document.createElement('li'); 21 | taskItem.textContent = task.description; 22 | taskItem.classList.add('list-group-item', 'task-item'); 23 | taskItem.dataset.taskIndex = index; 24 | taskItem.addEventListener('click', function() { 25 | highlightSelectedItem.call(taskItem, '.task-item', 'active'); 26 | fetchAndDisplayDevelopmentSteps(index, appId, dbName); 27 | }); 28 | tasksContainer.appendChild(taskItem); 29 | }); 30 | 31 | const appsContainer = document.getElementById('appsContainer'); 32 | const previousTasksContainer = appsContainer.querySelector('#tasksContainer'); 33 | if (previousTasksContainer) { 34 | appsContainer.replaceChild(tasksContainer, previousTasksContainer); 35 | } else { 36 | appsContainer.appendChild(tasksHeading); 37 | appsContainer.appendChild(tasksContainer); 38 | } 39 | } 40 | 41 | export function displayNoDevelopmentStepsMessage() { 42 | let stepsContainer = document.getElementById('stepsContainer'); 43 | if (!stepsContainer) { 44 | stepsContainer = document.createElement('div'); 45 | stepsContainer.id = 'stepsContainer'; 46 | stepsContainer.classList.add('mt-3'); 47 | 48 | const heading = document.createElement('h3'); 49 | heading.textContent = 'Development Steps'; 50 | stepsContainer.appendChild(heading); 51 | 52 | const appsContainer = document.getElementById('appsContainer'); 53 | appsContainer.appendChild(stepsContainer); 54 | } 55 | 56 | stepsContainer.innerHTML = ''; 57 | 58 | const heading = document.createElement('h3'); 59 | heading.textContent = 'Development Steps'; 60 | stepsContainer.appendChild(heading); 61 | 62 | const noStepsMessage = document.createElement('p'); 63 | noStepsMessage.textContent = 'No development steps found for this task.'; 64 | stepsContainer.appendChild(noStepsMessage); 65 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQLite_db_analysis_tool 2 | 3 | SQLite_db_analysis_tool is a web application designed to allow users to view and analyze data stored in SQLite databases. The tool provides a user-friendly frontend interface for uploading and managing databases, and it supports detailed inspection of database contents. 4 | 5 | ## Project Structure 6 | 7 | The project consists of a Node.js backend and a frontend utilizing HTML, CSS, and JavaScript. The backend is responsible for handling SQLite database file uploads, storage, and data retrieval. It exposes RESTful endpoints to the frontend for these operations. 8 | 9 | ### Backend 10 | 11 | The server is implemented in `app.js`, using the Express framework and includes additional routes defined in the `developmentPlans.js` and `developmentSteps.js` files. 12 | 13 | Key features include: 14 | - Database file upload and storage mechanism. 15 | - REST API endpoints to list databases, fetch apps, development plans, and steps. 16 | - Middleware for processing and categorizing development steps based on prompt path. 17 | 18 | ### Frontend 19 | 20 | The frontend is a set of static HTML, CSS, and JavaScript files. Users can select existing databases, upload new ones, and navigate through apps, development plans, and steps. 21 | 22 | Key interfaces include: 23 | - Database selection and upload. 24 | - Display of apps and their related development details. 25 | 26 | Key JavaScript modules responsible for these features are `database-selection.js`, `development-steps.js`, and `upload-handler.js`. 27 | 28 | ## Database Schema 29 | 30 | The SQLite database conforms to a particular schema where `app` contains app details, `development_planning` stores development tasks for apps, and `development_steps` holds individual steps for development tasks. 31 | 32 | ## Setup and Usage 33 | 34 | To set up the application: 35 | 36 | 1. Clone the repository. 37 | 2. Run `npm install` to install the required dependencies. 38 | 3. Start the server using `npm start`. 39 | 40 | Once the server is running, the front end can be accessed via a web browser to manage and analyze SQLite databases. 41 | 42 | ## API Reference 43 | 44 | The backend offers several API endpoints: 45 | 46 | - POST `/upload`: Upload a new SQLite database file. 47 | - GET `/databases`: List all uploaded SQLite database files. 48 | - GET `/apps`: Get all apps from the selected SQLite database. 49 | - GET `/development_plans`: Get all development tasks for a specific app. 50 | - GET `/development_steps`: Get all steps for a particular development task. 51 | 52 | ## Dependencies 53 | 54 | - `express` 55 | - `multer` for handling multipart/form-data. 56 | - `sqlite3` for interacting with SQLite databases. 57 | - `cors` for enabling CORS. 58 | - `body-parser` for parsing incoming request bodies. 59 | - `dotenv` for loading environment variables. 60 | 61 | For the full list of dependencies, refer to `package.json`. 62 | 63 | ## Contributing 64 | 65 | Contributions are welcome. For major changes, please open an issue first to discuss what you would like to change or add. 66 | 67 | ## License 68 | 69 | This project is licensed under the MIT License - see the `LICENSE` file for details. 70 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const multer = require('multer'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | const sqlite3 = require('sqlite3').verbose(); 7 | const bodyParser = require('body-parser'); 8 | const cors = require('cors'); 9 | const developmentPlansRouter = require('./routes/developmentPlans'); 10 | const developmentStepsRouter = require('./routes/developmentSteps'); 11 | const { handleOpenAIMessage } = require('./handlers/openaiMessageHandler'); 12 | const app = express(); 13 | 14 | app.use(express.static('public')); 15 | 16 | app.use(bodyParser.json()); 17 | 18 | app.use(cors()); 19 | 20 | const storage = multer.diskStorage({ 21 | destination: function (req, file, cb) { 22 | const uploadDir = 'uploads'; 23 | fs.mkdirSync(uploadDir, { recursive: true }); 24 | cb(null, uploadDir); 25 | }, 26 | filename: function (req, file, cb) { 27 | const dbName = req.body.dbname; 28 | const ext = path.extname(file.originalname).toLowerCase(); 29 | cb(null, dbName + ext); 30 | } 31 | }); 32 | 33 | const upload = multer({ 34 | storage: storage, 35 | fileFilter: function (req, file, cb) { 36 | if (!(/\.(sqlite|db)$/i).test(file.originalname)) { 37 | return cb(new Error('Only SQLite database files are allowed!')); 38 | } 39 | cb(null, true); 40 | } 41 | }); 42 | 43 | app.post('/upload', upload.single('dbfile'), (req, res) => { 44 | res.status(200).send('File uploaded successfully'); 45 | }, (error, req, res, next) => { 46 | res.status(400).send(error.message); 47 | }); 48 | 49 | app.get('/databases', (req, res) => { 50 | const uploadDir = 'uploads'; 51 | fs.readdir(uploadDir, (err, files) => { 52 | if (err) { 53 | res.status(500).send('Failed to list databases'); 54 | return; 55 | } 56 | const databases = files.filter(file => /\.(sqlite|db)$/i.test(file)).map(file => { 57 | return { name: file }; 58 | }); 59 | res.json(databases); 60 | }); 61 | }); 62 | 63 | app.get('/apps', (req, res) => { 64 | const dbName = req.query.db; 65 | if (!dbName) { 66 | return res.status(400).send('Database name is required'); 67 | } 68 | const dbPath = path.join('uploads', `${dbName}.sqlite`); 69 | if (!fs.existsSync(dbPath)) { 70 | return res.status(404).send('Database file not found'); 71 | } 72 | 73 | let db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => { 74 | if (err) { 75 | res.status(500).send("Error opening database: " + err.message); 76 | return; 77 | } 78 | }); 79 | 80 | db.serialize(() => { 81 | db.all("SELECT * FROM app", (err, rows) => { 82 | if (err) { 83 | res.status(500).send("Error reading from database: " + err.message); 84 | return; 85 | } 86 | res.json(rows); 87 | }); 88 | }); 89 | 90 | db.close((err) => { 91 | if (err) { 92 | console.error("Error closing database: " + err.message); 93 | } 94 | }); 95 | }); 96 | 97 | app.use(developmentPlansRouter); 98 | app.use(developmentStepsRouter); 99 | 100 | app.post('/submit_messages', handleOpenAIMessage); 101 | 102 | const port = process.env.PORT || 3000; 103 | app.listen(port, () => { 104 | console.log(`Server running on port ${port}`); 105 | }); -------------------------------------------------------------------------------- /routes/developmentSteps.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const sqlite3 = require('sqlite3').verbose(); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | const router = express.Router(); 7 | 8 | router.get('/development_steps', (req, res) => { 9 | const taskIndex = parseInt(req.query.task_index); 10 | const appId = req.query.app_id; 11 | const dbName = req.query.db; 12 | 13 | if (!Number.isInteger(taskIndex) || !appId) { 14 | return res.status(400).send('Task index integer and app ID are required'); 15 | } 16 | 17 | const dbPath = path.join('uploads', `${dbName}.sqlite`); 18 | if (!fs.existsSync(dbPath)) { 19 | return res.status(404).send('Database file not found'); 20 | } 21 | 22 | let db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => { 23 | if (err) { 24 | return res.status(500).send('Error opening database: ' + err.message); 25 | } 26 | }); 27 | 28 | const sql = ` 29 | SELECT * FROM development_steps 30 | WHERE app_id = ? 31 | ORDER BY id ASC 32 | `; 33 | 34 | db.all(sql, [appId], (err, rows) => { 35 | if (err) { 36 | return res.status(500).send('Error reading from database: ' + err.message); 37 | } 38 | 39 | function groupStepsByTask(steps) { 40 | const groupedSteps = []; 41 | let currentGroup = []; 42 | steps.forEach(step => { 43 | if (step.prompt_path === 'development/task/breakdown.prompt') { 44 | if (currentGroup.length > 0) { 45 | groupedSteps.push(currentGroup); 46 | } 47 | currentGroup = [step]; 48 | } else { 49 | currentGroup.push(step); 50 | } 51 | }); 52 | 53 | if (currentGroup.length > 0) { 54 | groupedSteps.push(currentGroup); 55 | } 56 | return groupedSteps; 57 | } 58 | 59 | const groupedSteps = groupStepsByTask(rows); 60 | if (taskIndex < 0 || taskIndex >= groupedSteps.length) { 61 | return res.status(404).send('Development task index out of bounds'); 62 | } 63 | 64 | res.json(groupedSteps[taskIndex]); 65 | }); 66 | 67 | db.close((err) => { 68 | if (err) console.error('Error closing database: ' + err.message); 69 | }); 70 | }); 71 | 72 | router.get('/development_step', (req, res) => { 73 | const stepId = req.query.id; 74 | const dbName = req.query.db; 75 | 76 | if (!stepId || !dbName) { 77 | return res.status(400).send('Step ID and database name are required'); 78 | } 79 | 80 | const dbPath = path.join('uploads', `${dbName}.sqlite`); 81 | if (!fs.existsSync(dbPath)) { 82 | return res.status(404).send('Database file not found'); 83 | } 84 | 85 | let db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => { 86 | if (err) { 87 | return res.status(500).send('Error opening database: ' + err.message); 88 | } 89 | }); 90 | 91 | const sql = 'SELECT prompt_path, messages, llm_response, prompt_data FROM development_steps WHERE id = ?'; 92 | 93 | db.get(sql, [stepId], (err, row) => { 94 | if (err) { 95 | return res.status(500).send('Error reading from database: ' + err.message); 96 | } 97 | if (row) { 98 | row.messages = typeof row.messages === 'string' ? JSON.parse(row.messages) : row.messages; 99 | row.llm_response = typeof row.llm_response === 'string' ? JSON.parse(row.llm_response) : row.llm_response; 100 | row.prompt_data = typeof row.prompt_data === 'string' ? JSON.parse(row.prompt_data) : row.prompt_data; 101 | res.json(row); 102 | } else { 103 | res.status(404).send('Development step not found'); 104 | } 105 | }); 106 | 107 | db.close((err) => { 108 | if (err) console.error('Error closing database: ' + err.message); 109 | }); 110 | }); 111 | 112 | module.exports = router; -------------------------------------------------------------------------------- /public/development-steps.js: -------------------------------------------------------------------------------- 1 | import { insertAfter } from './utils.js'; 2 | import { copyToClipboard } from './clipboard-operations.js'; 3 | import { showToast } from './toast-notifications.js'; 4 | 5 | function displayDevelopmentSteps(developmentSteps, taskIndex, appId, dbName) { 6 | console.log('Displaying development steps'); 7 | const stepsContainer = document.createElement('div'); 8 | stepsContainer.id = 'stepsContainer'; 9 | stepsContainer.classList.add('mt-3'); 10 | 11 | const heading = document.createElement('h3'); 12 | heading.classList.add('mb-3'); 13 | heading.textContent = 'Development Steps'; 14 | stepsContainer.appendChild(heading); 15 | 16 | developmentSteps.forEach((step, index) => { 17 | console.log(`Processing step ${index + 1}`); 18 | const stepItemContainer = document.createElement('div'); 19 | stepItemContainer.classList.add('card', 'mb-3'); 20 | const cardBody = document.createElement('div'); 21 | cardBody.classList.add('card-body'); 22 | stepItemContainer.appendChild(cardBody); 23 | 24 | const promptPathButton = document.createElement('button'); 25 | promptPathButton.classList.add('btn', 'btn-link'); 26 | promptPathButton.setAttribute('type', 'button'); 27 | promptPathButton.setAttribute('data-toggle', 'collapse'); 28 | promptPathButton.setAttribute('data-target', `#collapseStep${step.id}`); 29 | promptPathButton.setAttribute('aria-expanded', 'false'); 30 | promptPathButton.setAttribute('aria-controls', `collapseStep${step.id}`); 31 | const stepNumberSpan = document.createElement('span'); 32 | stepNumberSpan.classList.add('step-number'); 33 | stepNumberSpan.textContent = `Step ${step.id}:`; 34 | promptPathButton.appendChild(stepNumberSpan); 35 | const stepTitleText = document.createTextNode(` ${step.prompt_path}`); 36 | promptPathButton.appendChild(stepTitleText); 37 | promptPathButton.classList.add('card-title'); 38 | cardBody.appendChild(promptPathButton); 39 | 40 | const collapseDiv = document.createElement('div'); 41 | collapseDiv.id = `collapseStep${step.id}`; 42 | collapseDiv.classList.add('collapse'); 43 | 44 | // Parse llm_response if it is a string 45 | let llmResponse; 46 | try { 47 | llmResponse = typeof step.llm_response === 'string' ? JSON.parse(step.llm_response) : step.llm_response; 48 | } catch (err) { 49 | console.error(`An error occurred while parsing llm_response for step ${step.id}:`, err); 50 | llmResponse = {}; 51 | } 52 | 53 | if (llmResponse) { 54 | const llmResponseTextTitle = document.createElement('h6'); 55 | llmResponseTextTitle.textContent = 'LLM Response:'; 56 | llmResponseTextTitle.classList.add('mb-1'); 57 | collapseDiv.appendChild(llmResponseTextTitle); 58 | 59 | createTextareaWithCopyButton(collapseDiv, llmResponse.text, step.id); 60 | } 61 | 62 | // Parse messages if it is a string 63 | let messages; 64 | try { 65 | messages = typeof step.messages === 'string' ? JSON.parse(step.messages) : step.messages; 66 | } catch (err) { 67 | console.error(`An error occurred while parsing messages for step ${step.id}:`, err); 68 | messages = []; 69 | } 70 | 71 | const messagesContainer = document.createElement('div'); 72 | messagesContainer.classList.add('mb-2'); 73 | if (messages && messages.length > 0) { 74 | messages.forEach((message, messageIndex) => { 75 | const messageRoleLabel = document.createElement('h6'); 76 | messageRoleLabel.textContent = `Role: ${message.role}`; 77 | messageRoleLabel.classList.add('mb-1'); 78 | messagesContainer.appendChild(messageRoleLabel); 79 | 80 | createTextareaWithCopyButton(messagesContainer, message.content, step.id, message.role); 81 | }); 82 | 83 | const copyConversationButton = document.createElement('button'); 84 | copyConversationButton.textContent = 'COPY CONVERSATION'; 85 | copyConversationButton.classList.add('btn', 'btn-secondary', 'btn-sm', 'copy-conversation-btn'); 86 | copyConversationButton.disabled = messages.length === 0; 87 | copyConversationButton.addEventListener('click', function() { 88 | try { 89 | const stepData = { 90 | prompt_path: step.prompt_path, 91 | messages: messages, 92 | llm_response: llmResponse, 93 | prompt_data: step.prompt_data 94 | }; 95 | const stepDataString = JSON.stringify(stepData, null, 2); // Convert the object to a JSON string 96 | copyToClipboard(stepDataString); 97 | } catch (error) { 98 | showToast('Failed to copy step data. ' + error.message, 'danger'); 99 | } 100 | }); 101 | messagesContainer.appendChild(copyConversationButton); 102 | } else { 103 | console.log('No messages to display for this step.'); 104 | const noMessagesText = document.createElement('p'); 105 | noMessagesText.textContent = 'No messages available for this step.'; 106 | messagesContainer.appendChild(noMessagesText); 107 | } 108 | 109 | const submitButton = createSubmitButton(step.id); 110 | messagesContainer.appendChild(submitButton); 111 | collapseDiv.appendChild(messagesContainer); 112 | 113 | // prompt_data processing is removed from UI rendering logic 114 | // Nothing to render for prompt_data in the UI, it is still copied by copyToClipboard function 115 | 116 | cardBody.appendChild(collapseDiv); 117 | stepsContainer.appendChild(stepItemContainer); 118 | }); 119 | 120 | const appsContainer = document.getElementById('appsContainer'); 121 | const previousStepsContainer = document.getElementById('stepsContainer'); 122 | if (previousStepsContainer) { 123 | console.log('Replacing old steps container with new one.'); 124 | appsContainer.replaceChild(stepsContainer, previousStepsContainer); 125 | } else { 126 | console.log('Appending new steps container.'); 127 | appsContainer.appendChild(stepsContainer); 128 | } 129 | } 130 | 131 | function createSubmitButton(stepId) { 132 | console.log(`Creating submit button for stepId: ${stepId}`); 133 | const submitButton = document.createElement('button'); 134 | submitButton.textContent = 'Submit'; 135 | submitButton.classList.add('btn', 'btn-primary', 'mt-2'); 136 | submitButton.setAttribute('data-step-id', stepId.toString()); 137 | submitButton.addEventListener('click', () => { 138 | console.log(`Submit button clicked for stepId: ${stepId}`); 139 | try { 140 | const messageContents = Array.from(document.querySelectorAll(`#collapseStep${stepId} .message-content`)).map(textarea => { 141 | const role = textarea.getAttribute('data-role'); 142 | 143 | if (!role) { 144 | throw new Error(`Missing role for message with content: ${textarea.value}`); 145 | } 146 | 147 | return { 148 | content: textarea.value, 149 | role: role 150 | }; 151 | }); 152 | submitMessages(stepId, messageContents); 153 | } catch (error) { 154 | console.error('Error preparing messages for submission:', error); 155 | alert('Error: One or more messages are missing a role. Please refresh the page and try again.'); 156 | } 157 | }); 158 | return submitButton; 159 | } 160 | 161 | function submitMessages(stepId, messages) { 162 | console.log(`Submitting messages for stepId: ${stepId}`); 163 | const data = { messages }; 164 | const submitButton = document.querySelector(`button[data-step-id="${stepId}"]`); 165 | const spinner = showSpinner(stepId, true); 166 | submitButton.disabled = true; 167 | submitButton.after(spinner); 168 | 169 | fetch(`/submit_messages`, { 170 | method: 'POST', 171 | headers: { 'Content-Type': 'application/json' }, 172 | body: JSON.stringify(data), 173 | }) 174 | .then(response => { 175 | showSpinner(stepId, false); 176 | submitButton.disabled = false; 177 | if (!response.ok) { 178 | throw new Error(`HTTP error! Status: ${response.status}`); 179 | } 180 | return response.json(); 181 | }) 182 | .then(responseData => { 183 | if (responseData.error) { 184 | throw new Error(responseData.error.message); 185 | } 186 | displayOpenAIResponse(stepId, responseData); 187 | }) 188 | .catch(error => { 189 | console.error('Error submitting messages to server:', error); 190 | showSpinner(stepId, false); 191 | submitButton.disabled = false; 192 | displayApiError(error, stepId); 193 | }); 194 | } 195 | 196 | function showSpinner(stepId, shouldShow) { 197 | console.log(`Showing spinner for stepId: ${stepId}, shouldShow: ${shouldShow}`); 198 | let spinner = document.querySelector(`#spinner-${stepId}`); 199 | 200 | if (shouldShow) { 201 | if (!spinner) { 202 | spinner = document.createElement('div'); 203 | spinner.id = `spinner-${stepId}`; 204 | spinner.classList.add('spinner-border', 'text-primary', 'mt-2'); 205 | spinner.setAttribute('role', 'status'); 206 | const spinnerLabel = document.createElement('span'); 207 | spinnerLabel.classList.add('sr-only'); 208 | spinnerLabel.textContent = 'Loading...'; 209 | spinner.appendChild(spinnerLabel); 210 | } 211 | spinner.style.display = 'inline-block'; 212 | } else if (spinner) { 213 | spinner.style.display = 'none'; 214 | } 215 | 216 | return spinner; 217 | } 218 | 219 | function displayOpenAIResponse(stepId, responseData) { 220 | console.log(`Displaying OpenAI response for stepId: ${stepId}`, responseData); 221 | let combinedMessage = ''; 222 | if (responseData.choices && responseData.choices.length) { 223 | combinedMessage = responseData.choices.map(choice => choice.message.content).join("\n"); 224 | } 225 | const aiResponseTextarea = createOrUpdateTextArea(`ai-response-textarea-${stepId}`, combinedMessage, true); 226 | 227 | const submitButton = document.querySelector(`button[data-step-id="${stepId}"]`); 228 | insertAfter(aiResponseTextarea, submitButton); 229 | } 230 | 231 | function createTextareaWithCopyButton(containerElement, textValue, stepId, role = 'user') { 232 | const textarea = document.createElement('textarea'); 233 | textarea.value = textValue; 234 | textarea.classList.add('form-control', 'mb-1', 'message-content'); 235 | textarea.setAttribute('data-role', role); 236 | containerElement.appendChild(textarea); 237 | 238 | const copyButton = document.createElement('button'); 239 | copyButton.textContent = 'Copy'; 240 | copyButton.classList.add('btn', 'btn-secondary', 'btn-sm', 'ml-2'); 241 | copyButton.addEventListener('click', function() { 242 | try { 243 | copyToClipboard(textarea.value); 244 | console.log(`Content of role ${role} copied to clipboard from step id ${stepId}.`); 245 | } catch (error) { 246 | console.error(`Error copying content from step id ${stepId}:`, error.stack || error); 247 | showToast('Failed to copy data', 'danger'); 248 | } 249 | }); 250 | containerElement.appendChild(copyButton); 251 | } 252 | 253 | function createOrUpdateTextArea(id, value, disabled) { 254 | let textarea = document.getElementById(id); 255 | if (!textarea) { 256 | textarea = document.createElement('textarea'); 257 | textarea.id = id; 258 | textarea.classList.add('form-control', 'mt-2', 'ai-response-textarea'); 259 | } 260 | textarea.value = value; 261 | textarea.disabled = disabled; 262 | return textarea; 263 | } 264 | 265 | function displayApiError(error, stepId) { 266 | console.error(`Displaying API error for stepId: ${stepId}`, error); 267 | const errorMessage = `Error: ${error.message}`; 268 | const errorTextarea = createOrUpdateTextArea(`ai-response-textarea-${stepId}`, errorMessage, true); 269 | 270 | const submitButton = document.querySelector(`button[data-step-id="${stepId}"]`); 271 | insertAfter(errorTextarea, submitButton); 272 | } 273 | 274 | function fetchAndDisplayDevelopmentSteps(taskIndex, appId, dbName) { 275 | console.log(`Fetching and displaying development steps for taskIndex: ${taskIndex}, appId: ${appId}`); 276 | const xhr = new XMLHttpRequest(); 277 | xhr.open('GET', `/development_steps?task_index=${encodeURIComponent(taskIndex)}&app_id=${encodeURIComponent(appId)}&db=${encodeURIComponent(dbName)}`, true); 278 | xhr.onload = function() { 279 | if (xhr.status === 200) { 280 | const response = JSON.parse(xhr.responseText); 281 | if (response.length === 0 || (typeof response === 'string' && response.includes('Development task index out of bounds'))) { 282 | displayNoDevelopmentStepsMessage(); 283 | } else { 284 | displayDevelopmentSteps(response, taskIndex, appId, dbName); 285 | } 286 | } else { 287 | console.error('Failed to load development steps for the selected task', xhr.statusText); 288 | displayNoDevelopmentStepsMessage(); 289 | } 290 | }; 291 | xhr.onerror = function(e) { 292 | console.error('An error occurred during the Ajax request.', e); 293 | displayNoDevelopmentStepsMessage(); 294 | }; 295 | xhr.send(); 296 | } 297 | 298 | function displayNoDevelopmentStepsMessage() { 299 | console.log('Displaying message: No development steps found for this task.'); 300 | let stepsContainer = document.getElementById('stepsContainer'); 301 | if (!stepsContainer) { 302 | stepsContainer = document.createElement('div'); 303 | stepsContainer.id = 'stepsContainer'; 304 | stepsContainer.classList.add('mt-3'); 305 | 306 | const heading = document.createElement('h3'); 307 | heading.textContent = 'Development Steps'; 308 | stepsContainer.appendChild(heading); 309 | 310 | const appsContainer = document.getElementById('appsContainer'); 311 | appsContainer.appendChild(stepsContainer); 312 | } 313 | 314 | stepsContainer.innerHTML = ''; 315 | 316 | const heading = document.createElement('h3'); 317 | heading.textContent = 'Development Steps'; 318 | stepsContainer.appendChild(heading); 319 | 320 | const noStepsMessage = document.createElement('p'); 321 | noStepsMessage.textContent = 'No development steps found for this task.'; 322 | stepsContainer.appendChild(noStepsMessage); 323 | } 324 | 325 | export { fetchAndDisplayDevelopmentSteps, displayDevelopmentSteps, createSubmitButton }; --------------------------------------------------------------------------------