├── 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 |
17 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
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 };
--------------------------------------------------------------------------------