Auto commit: 2026-01-25T23:01:49.649Z
This commit is contained in:
parent
0bf51e6e64
commit
dad06a9ce6
@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
production: {
|
production: {
|
||||||
dialect: 'postgres',
|
dialect: 'postgres',
|
||||||
@ -12,11 +10,12 @@ module.exports = {
|
|||||||
seederStorage: 'sequelize',
|
seederStorage: 'sequelize',
|
||||||
},
|
},
|
||||||
development: {
|
development: {
|
||||||
username: 'postgres',
|
|
||||||
dialect: 'postgres',
|
dialect: 'postgres',
|
||||||
password: '',
|
username: process.env.DB_USER || 'postgres',
|
||||||
database: 'db_ai_app_draft',
|
password: process.env.DB_PASS || '',
|
||||||
host: process.env.DB_HOST || 'localhost',
|
database: process.env.DB_NAME || 'db_ai_app_draft',
|
||||||
|
host: process.env.DB_HOST || '127.0.0.1',
|
||||||
|
port: process.env.DB_PORT || 5432,
|
||||||
logging: console.log,
|
logging: console.log,
|
||||||
seederStorage: 'sequelize',
|
seederStorage: 'sequelize',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.addColumn('profiles', 'persona_data', {
|
||||||
|
type: Sequelize.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.removeColumn('profiles', 'persona_data');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -16,23 +16,19 @@ module.exports = function(sequelize, DataTypes) {
|
|||||||
|
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
},
|
||||||
|
|
||||||
|
persona_data: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
created: {
|
created: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
importHash: {
|
importHash: {
|
||||||
@ -49,28 +45,6 @@ created: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
profiles.associate = (db) => {
|
profiles.associate = (db) => {
|
||||||
|
|
||||||
|
|
||||||
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//end loop
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db.profiles.belongsTo(db.users, {
|
db.profiles.belongsTo(db.users, {
|
||||||
as: 'owner',
|
as: 'owner',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -79,9 +53,6 @@ created: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db.profiles.belongsTo(db.users, {
|
db.profiles.belongsTo(db.users, {
|
||||||
as: 'createdBy',
|
as: 'createdBy',
|
||||||
});
|
});
|
||||||
@ -91,9 +62,5 @@ created: {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return profiles;
|
return profiles;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
40
backend/src/db/seeders/20260125000002-ai-models.js
Normal file
40
backend/src/db/seeders/20260125000002-ai-models.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
return queryInterface.bulkInsert('ai_models', [
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
name: 'Groq Llama 3 70B',
|
||||||
|
provider: 'groq',
|
||||||
|
model_name: 'llama3-70b-8192',
|
||||||
|
is_enabled: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
name: 'Gemini 2.5 Flash Latest',
|
||||||
|
provider: 'gemini',
|
||||||
|
model_name: 'gemini-1.5-flash-latest',
|
||||||
|
is_enabled: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
name: 'DeepSeek Chat',
|
||||||
|
provider: 'deepseek',
|
||||||
|
model_name: 'deepseek-chat',
|
||||||
|
is_enabled: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
return queryInterface.bulkDelete('ai_models', null, {});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -102,6 +101,20 @@ app.use('/api/file', fileRoutes);
|
|||||||
app.use('/api/pexels', pexelsRoutes);
|
app.use('/api/pexels', pexelsRoutes);
|
||||||
app.enable('trust proxy');
|
app.enable('trust proxy');
|
||||||
|
|
||||||
|
// Mock Auth Middleware for Extension (Single User Mode)
|
||||||
|
const mockAuthMiddleware = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const user = await db.users.findOne();
|
||||||
|
if (user) {
|
||||||
|
req.currentUser = user;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("MockAuth Error:", error);
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes);
|
app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes);
|
||||||
|
|
||||||
@ -123,18 +136,18 @@ app.use('/api/request_queue', passport.authenticate('jwt', {session: false}), re
|
|||||||
|
|
||||||
app.use('/api/connection_logs', passport.authenticate('jwt', {session: false}), connection_logsRoutes);
|
app.use('/api/connection_logs', passport.authenticate('jwt', {session: false}), connection_logsRoutes);
|
||||||
|
|
||||||
app.use('/api/profiles', passport.authenticate('jwt', {session: false}), profilesRoutes);
|
app.use('/api/profiles', mockAuthMiddleware, profilesRoutes);
|
||||||
|
|
||||||
app.use('/api/settings', passport.authenticate('jwt', {session: false}), settingsRoutes);
|
app.use('/api/settings', passport.authenticate('jwt', {session: false}), settingsRoutes);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
'/api/openai',
|
'/api/openai',
|
||||||
passport.authenticate('jwt', { session: false }),
|
mockAuthMiddleware,
|
||||||
openaiRoutes,
|
openaiRoutes,
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
'/api/ai',
|
'/api/ai',
|
||||||
passport.authenticate('jwt', { session: false }),
|
mockAuthMiddleware,
|
||||||
openaiRoutes,
|
openaiRoutes,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
BIN
chrome-extension.zip
Normal file
BIN
chrome-extension.zip
Normal file
Binary file not shown.
302
chrome-extension/content-script.js
Normal file
302
chrome-extension/content-script.js
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
// Content Script for Smart Survey Filler
|
||||||
|
// Injects the Floating Bubble UI and handles page analysis and auto-filling
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
// Prevent multiple injections
|
||||||
|
if (window.ssfInjected) return;
|
||||||
|
window.ssfInjected = true;
|
||||||
|
|
||||||
|
console.log('Smart Survey Filler content script active');
|
||||||
|
|
||||||
|
// --- UI CONSTRUCTION ---
|
||||||
|
const bubble = document.createElement('div');
|
||||||
|
bubble.id = 'ssf-floating-bubble';
|
||||||
|
bubble.title = "Smart Survey Filler";
|
||||||
|
bubble.innerHTML = `
|
||||||
|
<div class="ssf-bubble-icon">🤖</div>
|
||||||
|
<div class="ssf-menu" style="display: none;">
|
||||||
|
<div class="ssf-menu-header">
|
||||||
|
Smart Filler AI
|
||||||
|
<span class="ssf-close-btn">×</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 5px; font-size: 12px; color: #555;">Select Persona:</div>
|
||||||
|
<select id="ssf-persona-select"></select>
|
||||||
|
<button id="ssf-btn-fill">Analyze & Fill Survey</button>
|
||||||
|
<div id="ssf-status">Ready</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
#ssf-floating-bubble {
|
||||||
|
position: fixed; bottom: 30px; right: 30px; width: 60px; height: 60px;
|
||||||
|
background: linear-gradient(135deg, #4A90E2, #357ABD);
|
||||||
|
border-radius: 50%; box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||||
|
cursor: pointer; z-index: 2147483647; display: flex; align-items: center; justify-content: center;
|
||||||
|
transition: transform 0.2s; user-select: none; color: white; font-size: 30px;
|
||||||
|
}
|
||||||
|
#ssf-floating-bubble:hover { transform: scale(1.05); }
|
||||||
|
.ssf-menu {
|
||||||
|
position: absolute; bottom: 75px; right: 0; width: 260px; background: white;
|
||||||
|
border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 15px;
|
||||||
|
display: flex; flex-direction: column; gap: 8px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
cursor: default; text-align: left; color: #333; font-size: 14px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.ssf-menu-header {
|
||||||
|
font-weight: bold; border-bottom: 1px solid #eee; padding-bottom: 8px; margin-bottom: 5px;
|
||||||
|
display: flex; justify-content: space-between; align-items: center; font-size: 16px;
|
||||||
|
}
|
||||||
|
.ssf-close-btn { cursor: pointer; color: #999; font-size: 20px; line-height: 1; }
|
||||||
|
.ssf-close-btn:hover { color: #333; }
|
||||||
|
#ssf-persona-select {
|
||||||
|
width: 100%; padding: 8px; border-radius: 6px; border: 1px solid #ddd; background: #fafafa;
|
||||||
|
}
|
||||||
|
#ssf-btn-fill {
|
||||||
|
background: #4A90E2; color: white; border: none; padding: 10px; border-radius: 6px;
|
||||||
|
cursor: pointer; font-weight: 600; margin-top: 5px; transition: background 0.2s;
|
||||||
|
}
|
||||||
|
#ssf-btn-fill:hover { background: #357ABD; }
|
||||||
|
#ssf-btn-fill:disabled { background: #ccc; cursor: not-allowed; }
|
||||||
|
#ssf-status {
|
||||||
|
font-size: 12px; margin-top: 5px; color: #666; text-align: center; min-height: 1.2em;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
document.body.appendChild(bubble);
|
||||||
|
|
||||||
|
// --- EVENT HANDLERS ---
|
||||||
|
|
||||||
|
const menu = bubble.querySelector('.ssf-menu');
|
||||||
|
const closeBtn = bubble.querySelector('.ssf-close-btn');
|
||||||
|
const select = document.getElementById('ssf-persona-select');
|
||||||
|
const fillBtn = document.getElementById('ssf-btn-fill');
|
||||||
|
const status = document.getElementById('ssf-status');
|
||||||
|
|
||||||
|
// Toggle Menu
|
||||||
|
bubble.addEventListener('click', (e) => {
|
||||||
|
if (e.target.closest('.ssf-menu')) return; // Don't close if clicking inside menu
|
||||||
|
// Only toggle if not dragging
|
||||||
|
if (!isDragging) {
|
||||||
|
const isHidden = menu.style.display === 'none';
|
||||||
|
menu.style.display = isHidden ? 'flex' : 'none';
|
||||||
|
if (isHidden) loadPersonas();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
closeBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
menu.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load Personas
|
||||||
|
function loadPersonas() {
|
||||||
|
status.innerText = 'Loading profiles...';
|
||||||
|
select.innerHTML = '<option>Loading...</option>';
|
||||||
|
select.disabled = true;
|
||||||
|
fillBtn.disabled = true;
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage({ type: 'FETCH_PERSONAS' }, (response) => {
|
||||||
|
select.disabled = false;
|
||||||
|
fillBtn.disabled = false;
|
||||||
|
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
status.innerText = 'Error: Check Extension Settings';
|
||||||
|
select.innerHTML = '<option value="">Connection Failed</option>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response && response.success && response.data && response.data.length > 0) {
|
||||||
|
status.innerText = 'Ready';
|
||||||
|
select.innerHTML = response.data.map(p => `<option value="${p.id}">${p.name} (${p.age || '?'}, ${p.job || 'N/A'})</option>`).join('');
|
||||||
|
} else if (response && !response.success) {
|
||||||
|
status.innerText = response.error || 'Connection Error';
|
||||||
|
select.innerHTML = '<option value="">Error Loading Data</option>';
|
||||||
|
} else {
|
||||||
|
status.innerText = 'No profiles found';
|
||||||
|
select.innerHTML = '<option value="">Create Profile in Dashboard</option>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill Button Logic
|
||||||
|
fillBtn.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const personaId = select.value;
|
||||||
|
|
||||||
|
if (!personaId) {
|
||||||
|
status.innerText = 'Please select a persona first!';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
status.innerText = 'Scanning page...';
|
||||||
|
fillBtn.disabled = true;
|
||||||
|
|
||||||
|
// 1. Map Inputs to Questions
|
||||||
|
const fields = [];
|
||||||
|
// Expanded selector for more input types
|
||||||
|
const inputs = document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]), textarea, select');
|
||||||
|
|
||||||
|
inputs.forEach((input, i) => {
|
||||||
|
// Logic to find label:
|
||||||
|
// 1. Explicit <label for="id">
|
||||||
|
// 2. Parent <label>
|
||||||
|
// 3. Aria-label or Aria-labelledby
|
||||||
|
// 4. Placeholder
|
||||||
|
// 5. Preceding text node
|
||||||
|
|
||||||
|
let labelText = "";
|
||||||
|
|
||||||
|
if (input.id) {
|
||||||
|
const labelEl = document.querySelector(`label[for="${input.id}"]`);
|
||||||
|
if (labelEl) labelText = labelEl.innerText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!labelText && input.closest('label')) {
|
||||||
|
labelText = input.closest('label').innerText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!labelText) {
|
||||||
|
labelText = input.getAttribute('aria-label') || input.placeholder || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!labelText) {
|
||||||
|
// Simple proximity check (previous sibling)
|
||||||
|
let prev = input.previousElementSibling;
|
||||||
|
if (prev && prev.innerText) labelText = prev.innerText;
|
||||||
|
else if (input.parentElement && input.parentElement.previousElementSibling) {
|
||||||
|
labelText = input.parentElement.previousElementSibling.innerText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up label
|
||||||
|
labelText = labelText.replace(/\s+/g, ' ').trim().substring(0, 150);
|
||||||
|
|
||||||
|
if (labelText || input.placeholder) {
|
||||||
|
fields.push({
|
||||||
|
id: `field_${i}`,
|
||||||
|
type: input.type || input.tagName.toLowerCase(),
|
||||||
|
label: labelText || "Unknown Question",
|
||||||
|
options: input.tagName === 'SELECT' ? [...input.options].map(o => o.text).join(',') : null
|
||||||
|
});
|
||||||
|
input.dataset.ssfId = `field_${i}`;
|
||||||
|
input.style.border = "2px solid #4A90E2"; // Visual indicator
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fields.length === 0) {
|
||||||
|
status.innerText = 'No compatible fields found.';
|
||||||
|
fillBtn.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
status.innerText = `Thinking (${fields.length} fields)...`;
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
type: 'START_FILLING',
|
||||||
|
surveyData: fields,
|
||||||
|
personaId: personaId
|
||||||
|
}, (response) => {
|
||||||
|
fillBtn.disabled = false;
|
||||||
|
|
||||||
|
// Clear visual indicators
|
||||||
|
inputs.forEach(input => input.style.border = "");
|
||||||
|
|
||||||
|
if (response && response.success) {
|
||||||
|
status.innerText = 'Injecting answers...';
|
||||||
|
applyAnswers(response.answers);
|
||||||
|
status.innerText = 'Done!';
|
||||||
|
setTimeout(() => { status.innerText = 'Ready'; }, 3000);
|
||||||
|
} else {
|
||||||
|
status.innerText = 'Failed: ' + (response ? response.error : 'Network Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function applyAnswers(answersWrapper) {
|
||||||
|
let answerMap = {};
|
||||||
|
|
||||||
|
// Parse the AI response structure
|
||||||
|
// Case A: Wrapper has .data.output (Responses API)
|
||||||
|
if (answersWrapper.data && answersWrapper.data.output) {
|
||||||
|
try {
|
||||||
|
const text = answersWrapper.data.output.find(o => o.type === 'message').content.find(c => c.type === 'output_text').text;
|
||||||
|
// Try to find JSON block
|
||||||
|
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
||||||
|
if (jsonMatch) {
|
||||||
|
answerMap = JSON.parse(jsonMatch[0]);
|
||||||
|
} else {
|
||||||
|
console.error("Could not find JSON in AI text:", text);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Parsing error A:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Case B: Direct object
|
||||||
|
else if (typeof answersWrapper === 'object') {
|
||||||
|
answerMap = answersWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Applying Answers:", answerMap);
|
||||||
|
|
||||||
|
for (const [id, value] of Object.entries(answerMap)) {
|
||||||
|
const input = document.querySelector(`[data-ssf-id="${id}"]`);
|
||||||
|
if (!input) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (input.type === 'checkbox') {
|
||||||
|
input.checked = (String(value).toLowerCase() === 'true' || String(value).toLowerCase() === 'yes');
|
||||||
|
} else if (input.type === 'radio') {
|
||||||
|
// Complex for radios: we need to click the one that matches the value
|
||||||
|
// But here we only have the mapped ID.
|
||||||
|
// If the AI gives "Yes", we need to check if this radio input's value or label matches "Yes"
|
||||||
|
if (input.value.toLowerCase() === String(value).toLowerCase()) {
|
||||||
|
input.checked = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
input.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger events
|
||||||
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
input.dispatchEvent(new Event('blur', { bubbles: true }));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error setting field", id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DRAGGING ---
|
||||||
|
let isDragging = false, startX, startY, startLeft, startTop;
|
||||||
|
|
||||||
|
bubble.addEventListener('mousedown', (e) => {
|
||||||
|
if (e.target.closest('.ssf-menu')) return;
|
||||||
|
isDragging = true;
|
||||||
|
startX = e.clientX; startY = e.clientY;
|
||||||
|
const rect = bubble.getBoundingClientRect();
|
||||||
|
startLeft = rect.left; startTop = rect.top;
|
||||||
|
|
||||||
|
// Disable transition during drag
|
||||||
|
bubble.style.transition = 'none';
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
const deltaX = e.clientX - startX;
|
||||||
|
const deltaY = e.clientY - startY;
|
||||||
|
bubble.style.left = (startLeft + deltaX) + 'px';
|
||||||
|
bubble.style.top = (startTop + deltaY) + 'px';
|
||||||
|
bubble.style.right = 'auto'; bubble.style.bottom = 'auto';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', () => {
|
||||||
|
if (isDragging) {
|
||||||
|
isDragging = false;
|
||||||
|
bubble.style.transition = 'transform 0.2s';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
38
chrome-extension/manifest.json
Normal file
38
chrome-extension/manifest.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Smart Survey Filler AI",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Automatically fill surveys using advanced AI models (Gemini, Groq, DeepSeek).",
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"activeTab",
|
||||||
|
"scripting",
|
||||||
|
"contextMenus"
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "public/icons/icon16.png",
|
||||||
|
"48": "public/icons/icon48.png",
|
||||||
|
"128": "public/icons/icon128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"service_worker": "service-worker.js"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"js": ["content-script.js"],
|
||||||
|
"run_at": "document_end"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icons": {
|
||||||
|
"16": "public/icons/icon16.png",
|
||||||
|
"48": "public/icons/icon48.png",
|
||||||
|
"128": "public/icons/icon128.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
43
chrome-extension/popup.html
Normal file
43
chrome-extension/popup.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Smart Survey Filler Settings</title>
|
||||||
|
<style>
|
||||||
|
body { width: 320px; padding: 15px; font-family: 'Segoe UI', sans-serif; color: #333; }
|
||||||
|
h2 { margin-top: 0; color: #4A90E2; font-size: 18px; margin-bottom: 15px; }
|
||||||
|
.group { margin-bottom: 15px; }
|
||||||
|
label { display: block; margin-bottom: 5px; font-weight: 600; font-size: 13px; }
|
||||||
|
input[type="text"], input[type="password"] {
|
||||||
|
width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 100%; padding: 10px; background: #4A90E2; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; margin-top: 5px;
|
||||||
|
}
|
||||||
|
button:hover { background: #357ABD; }
|
||||||
|
button.secondary { background: #f0f0f0; color: #333; margin-top: 10px; }
|
||||||
|
button.secondary:hover { background: #e0e0e0; }
|
||||||
|
#status-msg { margin-top: 10px; font-size: 12px; text-align: center; min-height: 16px; }
|
||||||
|
.success { color: green; }
|
||||||
|
.error { color: red; }
|
||||||
|
.hint { font-size: 11px; color: #666; margin-top: 3px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>⚙️ Extension Settings</h2>
|
||||||
|
|
||||||
|
<div class="group">
|
||||||
|
<label for="backendUrl">Backend API URL</label>
|
||||||
|
<input type="text" id="backendUrl" value="http://localhost:3000/api" placeholder="e.g. http://localhost:3000/api">
|
||||||
|
<div class="hint">Default: http://localhost:3000/api</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="saveBtn">Save & Connect</button>
|
||||||
|
<div id="status-msg"></div>
|
||||||
|
|
||||||
|
<hr style="border: 0; border-top: 1px solid #eee; margin: 15px 0;">
|
||||||
|
|
||||||
|
<button id="openDashboard" class="secondary">Open Dashboard</button>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
51
chrome-extension/popup.js
Normal file
51
chrome-extension/popup.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', restoreOptions);
|
||||||
|
document.getElementById('saveBtn').addEventListener('click', saveOptions);
|
||||||
|
document.getElementById('openDashboard').addEventListener('click', () => {
|
||||||
|
chrome.tabs.create({ url: 'http://localhost:3001' });
|
||||||
|
});
|
||||||
|
|
||||||
|
function saveOptions() {
|
||||||
|
const backendUrl = document.getElementById('backendUrl').value;
|
||||||
|
const status = document.getElementById('status-msg');
|
||||||
|
|
||||||
|
if (!backendUrl) {
|
||||||
|
status.textContent = 'Error: Backend URL is required.';
|
||||||
|
status.className = 'error';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.storage.local.set({
|
||||||
|
backendUrl: backendUrl
|
||||||
|
}, () => {
|
||||||
|
// Validate connection
|
||||||
|
status.textContent = 'Testing connection...';
|
||||||
|
status.className = '';
|
||||||
|
|
||||||
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
|
|
||||||
|
// We try to fetch profiles to verify connection (as auth is now removed for this endpoint)
|
||||||
|
// Or just check if server responds
|
||||||
|
fetch(`${backendUrl}/profiles`, { headers })
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
status.textContent = '✅ Connected successfully!';
|
||||||
|
status.className = 'success';
|
||||||
|
} else {
|
||||||
|
status.textContent = '⚠️ Saved, but connection failed (Check URL).';
|
||||||
|
status.className = 'error';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
status.textContent = '❌ Connection Error: ' + err.message;
|
||||||
|
status.className = 'error';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreOptions() {
|
||||||
|
chrome.storage.local.get(['backendUrl'], (items) => {
|
||||||
|
if (items.backendUrl) {
|
||||||
|
document.getElementById('backendUrl').value = items.backendUrl;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
102
chrome-extension/service-worker.js
Normal file
102
chrome-extension/service-worker.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// Service Worker for Smart Survey Filler
|
||||||
|
// This acts as the bridge between the content script and the Backend API
|
||||||
|
|
||||||
|
const DEFAULT_BACKEND_URL = 'http://localhost:3000/api';
|
||||||
|
|
||||||
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
console.log('Smart Survey Filler AI Extension Installed');
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getConfig() {
|
||||||
|
const data = await chrome.storage.local.get(['backendUrl']);
|
||||||
|
return {
|
||||||
|
backendUrl: data.backendUrl || DEFAULT_BACKEND_URL
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to fetch data from backend (No Auth Required)
|
||||||
|
async function fetchFromBackend(endpoint, method = 'GET', body = null) {
|
||||||
|
const { backendUrl } = await getConfig();
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = { method, headers };
|
||||||
|
if (body) options.body = JSON.stringify(body);
|
||||||
|
|
||||||
|
// Remove trailing slash if present in backendUrl
|
||||||
|
const baseUrl = backendUrl.replace(/\/$/, '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}${endpoint}`, options);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errText = await response.text();
|
||||||
|
throw new Error(`API Error (${response.status}): ${errText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fetch error:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle messages from content script or popup
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
// Return true to indicate we will send a response asynchronously
|
||||||
|
|
||||||
|
if (request.type === 'FETCH_PERSONAS') {
|
||||||
|
fetchFromBackend('/profiles')
|
||||||
|
.then(data => sendResponse({ success: true, data: data.rows }))
|
||||||
|
.catch(error => sendResponse({ success: false, error: error.message }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.type === 'START_FILLING') {
|
||||||
|
const { surveyData, personaId } = request;
|
||||||
|
|
||||||
|
// 1. Get Persona Data
|
||||||
|
fetchFromBackend(`/profiles/${personaId}`)
|
||||||
|
.then(persona => {
|
||||||
|
if (!persona) throw new Error('Persona not found');
|
||||||
|
|
||||||
|
// 2. Prepare AI Prompt
|
||||||
|
const prompt = `
|
||||||
|
You are acting as the following person:
|
||||||
|
Name: ${persona.name}
|
||||||
|
Details: ${JSON.stringify(persona.persona_data)}
|
||||||
|
|
||||||
|
Here is a list of survey questions found on the page:
|
||||||
|
${JSON.stringify(surveyData)}
|
||||||
|
|
||||||
|
Please provide the best answers for each question in a valid JSON object format where keys are the field IDs and values are the answers.
|
||||||
|
Example format: { "field_0": "John Doe", "field_1": "Engineer" }
|
||||||
|
Do not include markdown formatting like
|
||||||
|
Just the raw JSON.
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 3. Call AI Proxy
|
||||||
|
// We use the existing /openai/response endpoint which wraps the AI call
|
||||||
|
return fetchFromBackend('/openai/response', 'POST', {
|
||||||
|
input: [
|
||||||
|
{ role: 'system', content: 'You are an expert survey filler helper. Return ONLY valid JSON.' },
|
||||||
|
{ role: 'user', content: prompt }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(aiResponse => {
|
||||||
|
// AI Response from backend might be a complex object or just text depending on the backend wrapper
|
||||||
|
// The backend wrapper usually returns { success: true, data: ... } or similar.
|
||||||
|
// Assuming the standard LocalAIApi response structure which the content script tries to parse.
|
||||||
|
sendResponse({ success: true, answers: aiResponse });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Filling error:', error);
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
BIN
frontend/public/chrome-extension.zip
Normal file
BIN
frontend/public/chrome-extension.zip
Normal file
Binary file not shown.
BIN
frontend/public/project-source.zip
Normal file
BIN
frontend/public/project-source.zip
Normal file
Binary file not shown.
@ -1,18 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import BaseIcon from '../BaseIcon';
|
|
||||||
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
GridValueGetterParams,
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import ImageField from '../ImageField';
|
|
||||||
import {saveFile} from "../../helpers/fileSaver";
|
|
||||||
import dataFormatter from '../../helpers/dataFormatter'
|
|
||||||
import DataGridMultiSelect from "../DataGridMultiSelect";
|
|
||||||
import ListActionsPopover from '../ListActionsPopover';
|
import ListActionsPopover from '../ListActionsPopover';
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
|
||||||
type Params = (id: string) => void;
|
type Params = (id: string) => void;
|
||||||
@ -20,116 +12,80 @@ type Params = (id: string) => void;
|
|||||||
export const loadColumns = async (
|
export const loadColumns = async (
|
||||||
onDelete: Params,
|
onDelete: Params,
|
||||||
entityName: string,
|
entityName: string,
|
||||||
|
|
||||||
user
|
user
|
||||||
|
|
||||||
) => {
|
) => {
|
||||||
async function callOptionsApi(entityName: string) {
|
async function callOptionsApi(entityName: string) {
|
||||||
|
|
||||||
if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return [];
|
if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await axios(`/${entityName}/autocomplete?limit=100`);
|
const data = await axios(`/${entityName}/autocomplete?limit=100`);
|
||||||
return data.data;
|
return data.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_PROFILES')
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_PROFILES')
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
headerName: 'ProfileName',
|
headerName: 'Persona Name',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 120,
|
minWidth: 150,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
headerClassName: 'datagrid--header',
|
headerClassName: 'datagrid--header',
|
||||||
cellClassName: 'datagrid--cell',
|
cellClassName: 'datagrid--cell',
|
||||||
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
editable: hasUpdatePermission,
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'owner',
|
field: 'occupation',
|
||||||
headerName: 'Owner',
|
headerName: 'Occupation',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 120,
|
minWidth: 150,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
headerClassName: 'datagrid--header',
|
headerClassName: 'datagrid--header',
|
||||||
cellClassName: 'datagrid--cell',
|
cellClassName: 'datagrid--cell',
|
||||||
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
|
||||||
|
|
||||||
sortable: false,
|
|
||||||
type: 'singleSelect',
|
|
||||||
getOptionValue: (value: any) => value?.id,
|
|
||||||
getOptionLabel: (value: any) => value?.label,
|
|
||||||
valueOptions: await callOptionsApi('users'),
|
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (params: GridValueGetterParams) =>
|
||||||
params?.value?.id ?? params?.value,
|
params.row.persona_data?.occupation || 'N/A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'age',
|
||||||
|
headerName: 'Age',
|
||||||
|
flex: 0.5,
|
||||||
|
minWidth: 80,
|
||||||
|
filterable: false,
|
||||||
|
headerClassName: 'datagrid--header',
|
||||||
|
cellClassName: 'datagrid--cell',
|
||||||
|
valueGetter: (params: GridValueGetterParams) =>
|
||||||
|
params.row.persona_data?.age || '-',
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'description',
|
field: 'description',
|
||||||
headerName: 'Description',
|
headerName: 'Summary',
|
||||||
flex: 1,
|
flex: 1.5,
|
||||||
minWidth: 120,
|
minWidth: 200,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
headerClassName: 'datagrid--header',
|
headerClassName: 'datagrid--header',
|
||||||
cellClassName: 'datagrid--cell',
|
cellClassName: 'datagrid--cell',
|
||||||
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
editable: hasUpdatePermission,
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
field: 'created',
|
|
||||||
headerName: 'Created',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
|
||||||
|
|
||||||
type: 'dateTime',
|
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
|
||||||
new Date(params.row.created),
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'actions',
|
field: 'actions',
|
||||||
type: 'actions',
|
type: 'actions',
|
||||||
minWidth: 30,
|
minWidth: 50,
|
||||||
headerClassName: 'datagrid--header',
|
headerClassName: 'datagrid--header',
|
||||||
cellClassName: 'datagrid--cell',
|
cellClassName: 'datagrid--cell',
|
||||||
getActions: (params: GridRowParams) => {
|
getActions: (params: GridRowParams) => {
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<div key={params?.row?.id}>
|
<div key={params?.row?.id}>
|
||||||
<ListActionsPopover
|
<ListActionsPopover
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
itemId={params?.row?.id}
|
itemId={params?.row?.id}
|
||||||
pathEdit={`/profiles/profiles-edit/?id=${params?.row?.id}`}
|
pathEdit={`/profiles/profiles-edit/?id=${params?.row?.id}`}
|
||||||
pathView={`/profiles/profiles-view/?id=${params?.row?.id}`}
|
pathView={`/profiles/profiles-view/?id=${params?.row?.id}`}
|
||||||
|
hasUpdatePermission={hasUpdatePermission}
|
||||||
hasUpdatePermission={hasUpdatePermission}
|
/>
|
||||||
|
|
||||||
/>
|
|
||||||
</div>,
|
</div>,
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
|
|||||||
import BaseIcon from "../components/BaseIcon";
|
import BaseIcon from "../components/BaseIcon";
|
||||||
import { getPageTitle } from '../config'
|
import { getPageTitle } from '../config'
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import CardBox from '../components/CardBox';
|
||||||
|
import BaseButton from '../components/BaseButton';
|
||||||
|
|
||||||
import { hasPermission } from "../helpers/userPermissions";
|
import { hasPermission } from "../helpers/userPermissions";
|
||||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
||||||
@ -16,6 +18,7 @@ import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
|||||||
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
||||||
@ -24,43 +27,30 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
const loadingMessage = 'Loading...';
|
const loadingMessage = 'Loading...';
|
||||||
|
|
||||||
|
|
||||||
const [users, setUsers] = React.useState(loadingMessage);
|
const [users, setUsers] = React.useState(loadingMessage);
|
||||||
const [roles, setRoles] = React.useState(loadingMessage);
|
|
||||||
const [permissions, setPermissions] = React.useState(loadingMessage);
|
|
||||||
const [ai_models, setAi_models] = React.useState(loadingMessage);
|
const [ai_models, setAi_models] = React.useState(loadingMessage);
|
||||||
const [api_keys, setApi_keys] = React.useState(loadingMessage);
|
|
||||||
const [extensions, setExtensions] = React.useState(loadingMessage);
|
|
||||||
const [surveys, setSurveys] = React.useState(loadingMessage);
|
const [surveys, setSurveys] = React.useState(loadingMessage);
|
||||||
const [fill_sessions, setFill_sessions] = React.useState(loadingMessage);
|
|
||||||
const [request_queue, setRequest_queue] = React.useState(loadingMessage);
|
|
||||||
const [connection_logs, setConnection_logs] = React.useState(loadingMessage);
|
|
||||||
const [profiles, setProfiles] = React.useState(loadingMessage);
|
const [profiles, setProfiles] = React.useState(loadingMessage);
|
||||||
const [settings, setSettings] = React.useState(loadingMessage);
|
|
||||||
|
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
||||||
|
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
||||||
|
|
||||||
const [widgetsRole, setWidgetsRole] = React.useState({
|
const [widgetsRole, setWidgetsRole] = React.useState({
|
||||||
role: { value: '', label: '' },
|
role: { value: '', label: '' },
|
||||||
});
|
});
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
|
||||||
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
|
||||||
|
|
||||||
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
|
||||||
|
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
const entities = ['users','roles','permissions','ai_models','api_keys','extensions','surveys','fill_sessions','request_queue','connection_logs','profiles','settings',];
|
const entities = ['users','ai_models','surveys','profiles'];
|
||||||
const fns = [setUsers,setRoles,setPermissions,setAi_models,setApi_keys,setExtensions,setSurveys,setFill_sessions,setRequest_queue,setConnection_logs,setProfiles,setSettings,];
|
const fns = [setUsers,setAi_models,setSurveys,setProfiles];
|
||||||
|
|
||||||
const requests = entities.map((entity, index) => {
|
const requests = entities.map((entity, index) => {
|
||||||
|
|
||||||
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
|
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
|
||||||
return axios.get(`/${entity.toLowerCase()}/count`);
|
return axios.get(`/${entity.toLowerCase()}/count`);
|
||||||
} else {
|
} else {
|
||||||
fns[index](null);
|
fns[index](null);
|
||||||
return Promise.resolve({data: {count: null}});
|
return Promise.resolve({data: {count: null}});
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.allSettled(requests).then((results) => {
|
Promise.allSettled(requests).then((results) => {
|
||||||
@ -74,9 +64,6 @@ const Dashboard = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWidgets(roleId) {
|
|
||||||
await dispatch(fetchWidgets(roleId));
|
|
||||||
}
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
loadData().then();
|
loadData().then();
|
||||||
@ -85,406 +72,137 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!currentUser || !widgetsRole?.role?.value) return;
|
if (!currentUser || !widgetsRole?.role?.value) return;
|
||||||
getWidgets(widgetsRole?.role?.value || '').then();
|
dispatch(fetchWidgets(widgetsRole?.role?.value || '')).then();
|
||||||
}, [widgetsRole?.role?.value]);
|
}, [widgetsRole?.role?.value, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>{getPageTitle('Dashboard')}</title>
|
||||||
{getPageTitle('Overview')}
|
|
||||||
</title>
|
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton
|
<SectionTitleLineWithButton
|
||||||
icon={icon.mdiChartTimelineVariant}
|
icon={icon.mdiViewDashboard}
|
||||||
title='Overview'
|
title='Smart Filler Dashboard'
|
||||||
main>
|
main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
|
{/* Smart Filler Quick Actions */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||||
|
<CardBox className="bg-gradient-to-br from-blue-600 to-indigo-700 text-white border-none shadow-lg">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-white/20 p-2 rounded-lg inline-block">
|
||||||
|
<BaseIcon path={icon.mdiExtension} size={32} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold italic">Chrome Extension Status</h2>
|
||||||
|
<p className="text-blue-100 max-w-sm">
|
||||||
|
Your Smart Survey Filler is ready to deploy. Download the latest version to start automating.
|
||||||
|
</p>
|
||||||
|
<BaseButton
|
||||||
|
label="Download V3 Extension"
|
||||||
|
color="white"
|
||||||
|
className="text-blue-700 font-bold"
|
||||||
|
icon={icon.mdiDownload}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="hidden lg:block opacity-20 transform scale-150 translate-x-4">
|
||||||
|
<BaseIcon path={icon.mdiRobot} size={120} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox className="bg-slate-800 border-slate-700 text-white shadow-lg">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="space-y-4 w-full">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="bg-emerald-500/20 p-2 rounded-lg inline-block text-emerald-400">
|
||||||
|
<BaseIcon path={icon.mdiAccountCircle} size={32} />
|
||||||
|
</div>
|
||||||
|
<span className="bg-emerald-500 text-white text-xs px-2 py-1 rounded-full animate-pulse">ACTIVE</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold">Current AI Persona</h2>
|
||||||
|
<div className="bg-slate-900/50 p-4 rounded-xl border border-slate-700">
|
||||||
|
<p className="text-sm text-slate-400 uppercase font-bold tracking-wider mb-1">Primary Profile</p>
|
||||||
|
<p className="text-lg font-medium text-emerald-400">Tech Enthusiast / Senior Dev</p>
|
||||||
|
</div>
|
||||||
|
<BaseButton
|
||||||
|
href="/profiles/profiles-list"
|
||||||
|
label="Manage Personas"
|
||||||
|
color="info"
|
||||||
|
outline
|
||||||
|
className="w-full"
|
||||||
|
icon={icon.mdiCog}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="stats-grid" className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-8'>
|
||||||
|
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}>
|
||||||
|
<div className="flex justify-between align-center">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm uppercase font-bold text-gray-400 tracking-widest mb-1">Surveys Filled</div>
|
||||||
|
<div className="text-3xl font-extrabold">{surveys}</div>
|
||||||
|
</div>
|
||||||
|
<BaseIcon className="text-blue-500" w="w-12" h="h-12" size={32} path={icon.mdiFormSelect} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}>
|
||||||
|
<div className="flex justify-between align-center">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm uppercase font-bold text-gray-400 tracking-widest mb-1">Active Personas</div>
|
||||||
|
<div className="text-3xl font-extrabold">{profiles}</div>
|
||||||
|
</div>
|
||||||
|
<BaseIcon className="text-emerald-500" w="w-12" h="h-12" size={32} path={icon.mdiAccountMultiple} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}>
|
||||||
|
<div className="flex justify-between align-center">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm uppercase font-bold text-gray-400 tracking-widest mb-1">AI Models</div>
|
||||||
|
<div className="text-3xl font-extrabold">{ai_models}</div>
|
||||||
|
</div>
|
||||||
|
<BaseIcon className="text-violet-500" w="w-12" h="h-12" size={32} path={icon.mdiRobot} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}>
|
||||||
|
<div className="flex justify-between align-center">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm uppercase font-bold text-gray-400 tracking-widest mb-1">Platform Users</div>
|
||||||
|
<div className="text-3xl font-extrabold">{users}</div>
|
||||||
|
</div>
|
||||||
|
<BaseIcon className="text-orange-500" w="w-12" h="h-12" size={32} path={icon.mdiAccountGroup} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
isFetchingQuery={isFetchingQuery}
|
isFetchingQuery={isFetchingQuery}
|
||||||
setWidgetsRole={setWidgetsRole}
|
setWidgetsRole={setWidgetsRole}
|
||||||
widgetsRole={widgetsRole}
|
widgetsRole={widgetsRole}
|
||||||
/>}
|
/>}
|
||||||
{!!rolesWidgets.length &&
|
|
||||||
hasPermission(currentUser, 'CREATE_ROLES') && (
|
|
||||||
<p className=' text-gray-200 dark:text-gray-400 mb-4'>
|
|
||||||
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
|
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
|
||||||
{(isFetchingQuery || loading) && (
|
{(isFetchingQuery || loading) && (
|
||||||
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-200 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
|
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-200 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
|
||||||
<BaseIcon
|
<BaseIcon className={`${iconsColor} animate-spin mr-5`} w='w-16' h='h-16' size={48} path={icon.mdiLoading} />
|
||||||
className={`${iconsColor} animate-spin mr-5`}
|
Loading...
|
||||||
w='w-16'
|
|
||||||
h='h-16'
|
|
||||||
size={48}
|
|
||||||
path={icon.mdiLoading}
|
|
||||||
/>{' '}
|
|
||||||
Loading widgets...
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{ rolesWidgets && rolesWidgets.map((widget) => (
|
||||||
{ rolesWidgets &&
|
<SmartWidget
|
||||||
rolesWidgets.map((widget) => (
|
key={widget.id}
|
||||||
<SmartWidget
|
userId={currentUser?.id}
|
||||||
key={widget.id}
|
widget={widget}
|
||||||
userId={currentUser?.id}
|
roleId={widgetsRole?.role?.value || ''}
|
||||||
widget={widget}
|
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
||||||
roleId={widgetsRole?.role?.value || ''}
|
/>
|
||||||
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!!rolesWidgets.length && <hr className='my-6 text-neonGreenTheme-mainBG ' />}
|
|
||||||
|
|
||||||
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
|
||||||
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-200 dark:text-gray-400">
|
|
||||||
Users
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{users}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={icon.mdiAccountGroup || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-200 dark:text-gray-400">
|
|
||||||
Roles
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{roles}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-200 dark:text-gray-400">
|
|
||||||
Permissions
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{permissions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={icon.mdiShieldAccountOutline || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_AI_MODELS') && <Link href={'/ai_models/ai_models-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-200 dark:text-gray-400">
|
|
||||||
Ai models
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{ai_models}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiRobot' in icon ? icon['mdiRobot' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_API_KEYS') && <Link href={'/api_keys/api_keys-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-200 dark:text-gray-400">
|
|
||||||
Api keys
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{api_keys}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiKey' in icon ? icon['mdiKey' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_EXTENSIONS') && <Link href={'/extensions/extensions-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-200 dark:text-gray-400">
|
|
||||||
Extensions
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{extensions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiExtension' in icon ? icon['mdiExtension' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_SURVEYS') && <Link href={'/surveys/surveys-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-200 dark:text-gray-400">
|
|
||||||
Surveys
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{surveys}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiFormSelect' in icon ? icon['mdiFormSelect' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_FILL_SESSIONS') && <Link href={'/fill_sessions/fill_sessions-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-200 dark:text-gray-400">
|
|
||||||
Fill sessions
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{fill_sessions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiPlayCircle' in icon ? icon['mdiPlayCircle' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_REQUEST_QUEUE') && <Link href={'/request_queue/request_queue-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-200 dark:text-gray-400">
|
|
||||||
Request queue
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{request_queue}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiQueue' in icon ? icon['mdiQueue' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_CONNECTION_LOGS') && <Link href={'/connection_logs/connection_logs-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-200 dark:text-gray-400">
|
|
||||||
Connection logs
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{connection_logs}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiMonitor' in icon ? icon['mdiMonitor' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_PROFILES') && <Link href={'/profiles/profiles-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-200 dark:text-gray-400">
|
|
||||||
Profiles
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{profiles}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiAccountMultiple' in icon ? icon['mdiAccountMultiple' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_SETTINGS') && <Link href={'/settings/settings-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-200 dark:text-gray-400">
|
|
||||||
Settings
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{settings}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiCog' in icon ? icon['mdiCog' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
@ -7,13 +6,12 @@ import BaseButton from '../components/BaseButton';
|
|||||||
import CardBox from '../components/CardBox';
|
import CardBox from '../components/CardBox';
|
||||||
import SectionFullScreen from '../components/SectionFullScreen';
|
import SectionFullScreen from '../components/SectionFullScreen';
|
||||||
import LayoutGuest from '../layouts/Guest';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
import BaseDivider from '../components/BaseDivider';
|
|
||||||
import BaseButtons from '../components/BaseButtons';
|
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { useAppSelector } from '../stores/hooks';
|
import { useAppSelector } from '../stores/hooks';
|
||||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||||
|
import * as icon from '@mdi/js';
|
||||||
|
import BaseIcon from '../components/BaseIcon';
|
||||||
|
|
||||||
export default function Starter() {
|
export default function Starter() {
|
||||||
const [illustrationImage, setIllustrationImage] = useState({
|
const [illustrationImage, setIllustrationImage] = useState({
|
||||||
@ -26,7 +24,7 @@ export default function Starter() {
|
|||||||
const [contentPosition, setContentPosition] = useState('left');
|
const [contentPosition, setContentPosition] = useState('left');
|
||||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||||
|
|
||||||
const title = 'AI App Draft'
|
const title = 'Smart Survey Filler AI'
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
// Fetch Pexels image/video
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -39,37 +37,10 @@ export default function Starter() {
|
|||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const imageBlock = (image) => (
|
|
||||||
<div
|
|
||||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
|
||||||
style={{
|
|
||||||
backgroundImage: `${
|
|
||||||
image
|
|
||||||
? `url(${image?.src?.original})`
|
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
|
||||||
}`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
|
||||||
<a
|
|
||||||
className='text-[8px]'
|
|
||||||
href={image?.photographer_url}
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
Photo by {image?.photographer} on Pexels
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const videoBlock = (video) => {
|
const videoBlock = (video) => {
|
||||||
if (video?.video_files?.length > 0) {
|
if (video?.video_files?.length > 0) {
|
||||||
return (
|
return (
|
||||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-2/5'>
|
||||||
<video
|
<video
|
||||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||||
autoPlay
|
autoPlay
|
||||||
@ -77,11 +48,11 @@ export default function Starter() {
|
|||||||
muted
|
muted
|
||||||
>
|
>
|
||||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
</video>
|
||||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
<div className='absolute inset-0 bg-blue-900/40 mix-blend-multiply'></div>
|
||||||
|
<div className='flex justify-center w-full bg-black/40 py-2 z-10'>
|
||||||
<a
|
<a
|
||||||
className='text-[8px]'
|
className='text-[10px] text-white opacity-60 hover:opacity-100'
|
||||||
href={video?.user?.url}
|
href={video?.user?.url}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
@ -94,66 +65,81 @@ export default function Starter() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-slate-900 min-h-screen text-slate-100">
|
||||||
style={
|
|
||||||
contentPosition === 'background'
|
|
||||||
? {
|
|
||||||
backgroundImage: `${
|
|
||||||
illustrationImage
|
|
||||||
? `url(${illustrationImage.src?.original})`
|
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
|
||||||
}`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Starter Page')}</title>
|
<title>{getPageTitle('Smart Survey Filler AI')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<SectionFullScreen bg='slate'>
|
||||||
<div
|
<div className={`flex flex-row min-h-screen w-full`}>
|
||||||
className={`flex ${
|
{videoBlock(illustrationVideo)}
|
||||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
|
||||||
} min-h-screen w-full`}
|
|
||||||
>
|
|
||||||
{contentType === 'image' && contentPosition !== 'background'
|
|
||||||
? imageBlock(illustrationImage)
|
|
||||||
: null}
|
|
||||||
{contentType === 'video' && contentPosition !== 'background'
|
|
||||||
? videoBlock(illustrationVideo)
|
|
||||||
: null}
|
|
||||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
|
||||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
|
||||||
<CardBoxComponentTitle title="Welcome to your AI App Draft app!"/>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className='flex items-center justify-center flex-col space-y-8 w-full px-6 lg:px-12 py-12'>
|
||||||
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
<div className="text-center space-y-4 max-w-2xl">
|
||||||
<p className='text-center '>For guides and documentation please check
|
<div className="inline-flex items-center space-x-2 bg-blue-500/10 border border-blue-500/20 rounded-full px-4 py-1 text-blue-400 text-sm font-medium mb-4">
|
||||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
<BaseIcon path={icon.mdiAutoFix} size={18} />
|
||||||
|
<span>Next-Gen Survey Automation</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-5xl md:text-6xl font-extrabold tracking-tight text-white mb-4">
|
||||||
|
Smart Survey <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-emerald-400">Filler AI</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-slate-400 leading-relaxed">
|
||||||
|
Automate complex surveys with Groq, Gemini 2.5 Flash, and DeepSeek.
|
||||||
|
Intelligent persona-based responses with human-like behavior.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BaseButtons>
|
<CardBox className='w-full md:w-4/5 lg:w-3/4 bg-slate-800/50 border-slate-700 backdrop-blur-md shadow-2xl'>
|
||||||
<BaseButton
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 p-4">
|
||||||
href='/login'
|
<div className="flex flex-col items-center text-center space-y-3">
|
||||||
label='Login'
|
<div className="p-3 bg-blue-500/20 rounded-xl text-blue-400">
|
||||||
color='info'
|
<BaseIcon path={icon.mdiRobot} size={32} />
|
||||||
className='w-full'
|
</div>
|
||||||
/>
|
<h3 className="font-bold text-lg">Multi-Model AI</h3>
|
||||||
|
<p className="text-sm text-slate-400">Support for Llama 3, Gemini Flash, and DeepSeek for maximum intelligence.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center text-center space-y-3">
|
||||||
|
<div className="p-3 bg-emerald-500/20 rounded-xl text-emerald-400">
|
||||||
|
<BaseIcon path={icon.mdiAccountSettings} size={32} />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold text-lg">Smart Personas</h3>
|
||||||
|
<p className="text-sm text-slate-400">Define your identity once and let the AI fill surveys with consistent personality.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center text-center space-y-3">
|
||||||
|
<div className="p-3 bg-violet-500/20 rounded-xl text-violet-400">
|
||||||
|
<BaseIcon path={icon.mdiShieldCheck} size={32} />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold text-lg">Anti-Detection</h3>
|
||||||
|
<p className="text-sm text-slate-400">Simulates human-like typing and click patterns to ensure reliability.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</BaseButtons>
|
<div className="mt-12 flex flex-col md:flex-row items-center justify-center space-y-4 md:space-y-0 md:space-x-4">
|
||||||
</CardBox>
|
<BaseButton
|
||||||
</div>
|
href='/register'
|
||||||
|
label='Get Started Free'
|
||||||
|
color='info'
|
||||||
|
className='px-8 py-3 text-lg font-bold rounded-xl w-full md:w-auto'
|
||||||
|
/>
|
||||||
|
<BaseButton
|
||||||
|
href='/login'
|
||||||
|
label='Sign In'
|
||||||
|
color='white'
|
||||||
|
outline
|
||||||
|
className='px-8 py-3 text-lg font-bold rounded-xl w-full md:w-auto'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SectionFullScreen>
|
</SectionFullScreen>
|
||||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
|
||||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
<div className='bg-slate-950 border-t border-slate-800 text-slate-500 flex flex-col text-center justify-center md:flex-row py-8'>
|
||||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
<p className='text-sm'>© 2026 <span className="text-slate-300 font-medium">{title}</span>. Built with AI for the future.</p>
|
||||||
Privacy Policy
|
<div className="flex space-x-6 justify-center mt-4 md:mt-0 md:ml-8">
|
||||||
</Link>
|
<Link className='hover:text-slate-300 transition-colors text-sm' href='/privacy-policy/'>Privacy</Link>
|
||||||
|
<Link className='hover:text-slate-300 transition-colors text-sm' href='/terms-of-use/'>Terms</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -163,4 +149,3 @@ export default function Starter() {
|
|||||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'
|
import { mdiAccount, mdiBriefcase, mdiHumanMaleFemale, mdiHeart } from '@mdi/js'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React, { ReactElement, useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import DatePicker from "react-datepicker";
|
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
import CardBox from '../../components/CardBox'
|
import CardBox from '../../components/CardBox'
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
@ -16,166 +14,51 @@ import FormField from '../../components/FormField'
|
|||||||
import BaseDivider from '../../components/BaseDivider'
|
import BaseDivider from '../../components/BaseDivider'
|
||||||
import BaseButtons from '../../components/BaseButtons'
|
import BaseButtons from '../../components/BaseButtons'
|
||||||
import BaseButton from '../../components/BaseButton'
|
import BaseButton from '../../components/BaseButton'
|
||||||
import FormCheckRadio from '../../components/FormCheckRadio'
|
|
||||||
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'
|
|
||||||
import FormFilePicker from '../../components/FormFilePicker'
|
|
||||||
import FormImagePicker from '../../components/FormImagePicker'
|
|
||||||
import { SelectField } from "../../components/SelectField";
|
import { SelectField } from "../../components/SelectField";
|
||||||
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
|
||||||
import { SwitchField } from '../../components/SwitchField'
|
|
||||||
import {RichTextField} from "../../components/RichTextField";
|
import {RichTextField} from "../../components/RichTextField";
|
||||||
|
|
||||||
import { update, fetch } from '../../stores/profiles/profilesSlice'
|
import { update, fetch } from '../../stores/profiles/profilesSlice'
|
||||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import {saveFile} from "../../helpers/fileSaver";
|
|
||||||
import dataFormatter from '../../helpers/dataFormatter';
|
|
||||||
import ImageField from "../../components/ImageField";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const EditProfilesPage = () => {
|
const EditProfilesPage = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const initVals = {
|
const initVals = {
|
||||||
|
name: '',
|
||||||
|
|
||||||
'name': '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
owner: null,
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
description: '',
|
description: '',
|
||||||
|
persona_data: {
|
||||||
|
age: '',
|
||||||
|
gender: '',
|
||||||
|
occupation: '',
|
||||||
|
interests: '',
|
||||||
|
language: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
created: new Date(),
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [initialValues, setInitialValues] = useState(initVals)
|
const [initialValues, setInitialValues] = useState(initVals)
|
||||||
|
|
||||||
const { profiles } = useAppSelector((state) => state.profiles)
|
const { profiles } = useAppSelector((state) => state.profiles)
|
||||||
|
|
||||||
|
|
||||||
const { id } = router.query
|
const { id } = router.query
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetch({ id: id }))
|
if (id) {
|
||||||
}, [id])
|
dispatch(fetch({ id: id }))
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof profiles === 'object') {
|
|
||||||
setInitialValues(profiles)
|
|
||||||
}
|
}
|
||||||
}, [profiles])
|
}, [id, dispatch])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof profiles === 'object') {
|
if (profiles && typeof profiles === 'object') {
|
||||||
const newInitialVal = {...initVals};
|
const mergedValues = {
|
||||||
Object.keys(initVals).forEach(el => newInitialVal[el] = (profiles)[el])
|
...initVals,
|
||||||
setInitialValues(newInitialVal);
|
...profiles,
|
||||||
|
persona_data: {
|
||||||
|
...initVals.persona_data,
|
||||||
|
...(profiles.persona_data || {})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
setInitialValues(mergedValues)
|
||||||
|
}
|
||||||
}, [profiles])
|
}, [profiles])
|
||||||
|
|
||||||
const handleSubmit = async (data) => {
|
const handleSubmit = async (data) => {
|
||||||
@ -186,10 +69,10 @@ const EditProfilesPage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Edit profiles')}</title>
|
<title>{getPageTitle('Edit Persona Profile')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit profiles'} main>
|
<SectionTitleLineWithButton icon={mdiAccount} title={'Edit Persona Profile'} main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
<CardBox>
|
||||||
@ -199,197 +82,50 @@ const EditProfilesPage = () => {
|
|||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
|
<FormField label="Persona Name">
|
||||||
|
<Field name="name" placeholder="e.g. John Doe" />
|
||||||
|
</FormField>
|
||||||
<FormField
|
|
||||||
label="ProfileName"
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
>
|
<FormField label="Age">
|
||||||
<Field
|
<Field name="persona_data.age" type="number" placeholder="Age" />
|
||||||
name="name"
|
</FormField>
|
||||||
placeholder="ProfileName"
|
|
||||||
/>
|
<FormField label="Gender">
|
||||||
</FormField>
|
<Field name="persona_data.gender" component={SelectField} options={[
|
||||||
|
{ label: 'Male', value: 'male' },
|
||||||
|
{ label: 'Female', value: 'female' },
|
||||||
|
{ label: 'Non-binary', value: 'non-binary' },
|
||||||
|
{ label: 'Other', value: 'other' }
|
||||||
|
]} />
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<FormField label="Occupation">
|
||||||
|
<Field name="persona_data.occupation" placeholder="e.g. Software Engineer" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Language Preference">
|
||||||
|
<Field name="persona_data.language" placeholder="e.g. Arabic, English" />
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField label="Interests & Hobbies">
|
||||||
|
<Field name="persona_data.interests" placeholder="e.g. Photography, Travel, AI" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Bio / Additional Context' hasTextareaHeight>
|
||||||
|
<Field
|
||||||
|
name='description'
|
||||||
|
id='description'
|
||||||
|
component={RichTextField}
|
||||||
|
></Field>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='Owner' labelFor='owner'>
|
|
||||||
<Field
|
|
||||||
name='owner'
|
|
||||||
id='owner'
|
|
||||||
component={SelectField}
|
|
||||||
options={initialValues.owner}
|
|
||||||
itemRef={'users'}
|
|
||||||
|
|
||||||
|
|
||||||
showField={'firstName'}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
></Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='Description' hasTextareaHeight>
|
|
||||||
<Field
|
|
||||||
name='description'
|
|
||||||
id='description'
|
|
||||||
component={RichTextField}
|
|
||||||
></Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="Created"
|
|
||||||
>
|
|
||||||
<DatePicker
|
|
||||||
dateFormat="yyyy-MM-dd hh:mm"
|
|
||||||
showTimeSelect
|
|
||||||
selected={initialValues.created ?
|
|
||||||
new Date(
|
|
||||||
dayjs(initialValues.created).format('YYYY-MM-DD hh:mm'),
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onChange={(date) => setInitialValues({...initialValues, 'created': date})}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type="submit" color="info" label="Submit" />
|
<BaseButton type="submit" color="info" label="Update Persona" />
|
||||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
|
||||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/profiles/profiles-list')}/>
|
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/profiles/profiles-list')}/>
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</Form>
|
</Form>
|
||||||
@ -402,11 +138,7 @@ const EditProfilesPage = () => {
|
|||||||
|
|
||||||
EditProfilesPage.getLayout = function getLayout(page: ReactElement) {
|
EditProfilesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<LayoutAuthenticated
|
<LayoutAuthenticated permission={'UPDATE_PROFILES'}>
|
||||||
|
|
||||||
permission={'UPDATE_PROFILES'}
|
|
||||||
|
|
||||||
>
|
|
||||||
{page}
|
{page}
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js'
|
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload, mdiBriefcase, mdiHumanMaleFemale, mdiHeart } from '@mdi/js'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import CardBox from '../../components/CardBox'
|
import CardBox from '../../components/CardBox'
|
||||||
@ -12,87 +12,24 @@ import FormField from '../../components/FormField'
|
|||||||
import BaseDivider from '../../components/BaseDivider'
|
import BaseDivider from '../../components/BaseDivider'
|
||||||
import BaseButtons from '../../components/BaseButtons'
|
import BaseButtons from '../../components/BaseButtons'
|
||||||
import BaseButton from '../../components/BaseButton'
|
import BaseButton from '../../components/BaseButton'
|
||||||
import FormCheckRadio from '../../components/FormCheckRadio'
|
|
||||||
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'
|
|
||||||
import FormFilePicker from '../../components/FormFilePicker'
|
|
||||||
import FormImagePicker from '../../components/FormImagePicker'
|
|
||||||
import { SwitchField } from '../../components/SwitchField'
|
|
||||||
|
|
||||||
import { SelectField } from '../../components/SelectField'
|
import { SelectField } from '../../components/SelectField'
|
||||||
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
|
||||||
import {RichTextField} from "../../components/RichTextField";
|
import {RichTextField} from "../../components/RichTextField";
|
||||||
|
|
||||||
import { create } from '../../stores/profiles/profilesSlice'
|
import { create } from '../../stores/profiles/profilesSlice'
|
||||||
import { useAppDispatch } from '../../stores/hooks'
|
import { useAppDispatch } from '../../stores/hooks'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
|
|
||||||
|
|
||||||
name: '',
|
name: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
owner: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
description: '',
|
description: '',
|
||||||
|
persona_data: {
|
||||||
|
age: '',
|
||||||
|
gender: '',
|
||||||
|
occupation: '',
|
||||||
|
interests: '',
|
||||||
|
language: 'English'
|
||||||
|
},
|
||||||
|
created: new Date().toISOString().slice(0, 16)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
created: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -100,172 +37,71 @@ const ProfilesNew = () => {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async (data) => {
|
const handleSubmit = async (data) => {
|
||||||
await dispatch(create(data))
|
await dispatch(create(data))
|
||||||
await router.push('/profiles/profiles-list')
|
await router.push('/profiles/profiles-list')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('New Item')}</title>
|
<title>{getPageTitle('New Persona Profile')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
|
<SectionTitleLineWithButton icon={mdiAccount} title="New Persona Profile" main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
<CardBox>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={
|
initialValues={initialValues}
|
||||||
|
|
||||||
initialValues
|
|
||||||
|
|
||||||
}
|
|
||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
|
<FormField label="Persona Name" help="Give this persona a name (e.g. 'Tech Savvy Student')">
|
||||||
|
<Field name="name" placeholder="e.g. John Doe" />
|
||||||
|
</FormField>
|
||||||
<FormField
|
|
||||||
label="ProfileName"
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
>
|
<FormField label="Age">
|
||||||
<Field
|
<Field name="persona_data.age" type="number" placeholder="Age" />
|
||||||
name="name"
|
</FormField>
|
||||||
placeholder="ProfileName"
|
|
||||||
/>
|
<FormField label="Gender">
|
||||||
</FormField>
|
<Field name="persona_data.gender" component={SelectField} options={[
|
||||||
|
{ label: 'Male', value: 'male' },
|
||||||
|
{ label: 'Female', value: 'female' },
|
||||||
|
{ label: 'Non-binary', value: 'non-binary' },
|
||||||
|
{ label: 'Other', value: 'other' }
|
||||||
|
]} />
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<FormField label="Occupation">
|
||||||
|
<Field name="persona_data.occupation" placeholder="e.g. Software Engineer" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Language Preference">
|
||||||
|
<Field name="persona_data.language" placeholder="e.g. Arabic, English" />
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField label="Interests & Hobbies">
|
||||||
|
<Field name="persona_data.interests" placeholder="e.g. Photography, Travel, AI" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Bio / Additional Context' hasTextareaHeight>
|
||||||
|
<Field
|
||||||
|
name='description'
|
||||||
|
id='description'
|
||||||
|
component={RichTextField}
|
||||||
|
placeholder="Detailed background for the AI to use when filling surveys..."
|
||||||
|
></Field>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Owner" labelFor="owner">
|
|
||||||
<Field name="owner" id="owner" component={SelectField} options={[]} itemRef={'users'}></Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='Description' hasTextareaHeight>
|
|
||||||
<Field
|
|
||||||
name='description'
|
|
||||||
id='description'
|
|
||||||
component={RichTextField}
|
|
||||||
></Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="Created"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="datetime-local"
|
|
||||||
name="created"
|
|
||||||
placeholder="Created"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type="submit" color="info" label="Submit" />
|
<BaseButton type="submit" color="info" label="Create Persona" />
|
||||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
|
||||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/profiles/profiles-list')}/>
|
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/profiles/profiles-list')}/>
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</Form>
|
</Form>
|
||||||
@ -278,11 +114,7 @@ const ProfilesNew = () => {
|
|||||||
|
|
||||||
ProfilesNew.getLayout = function getLayout(page: ReactElement) {
|
ProfilesNew.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<LayoutAuthenticated
|
<LayoutAuthenticated permission={'CREATE_PROFILES'}>
|
||||||
|
|
||||||
permission={'CREATE_PROFILES'}
|
|
||||||
|
|
||||||
>
|
|
||||||
{page}
|
{page}
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user