Auto commit: 2026-01-25T23:01:49.649Z
This commit is contained in:
parent
0bf51e6e64
commit
dad06a9ce6
@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
module.exports = {
|
||||
production: {
|
||||
dialect: 'postgres',
|
||||
@ -12,11 +10,12 @@ module.exports = {
|
||||
seederStorage: 'sequelize',
|
||||
},
|
||||
development: {
|
||||
username: 'postgres',
|
||||
dialect: 'postgres',
|
||||
password: '',
|
||||
database: 'db_ai_app_draft',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
username: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASS || '',
|
||||
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,
|
||||
seederStorage: 'sequelize',
|
||||
},
|
||||
@ -30,4 +29,4 @@ module.exports = {
|
||||
logging: console.log,
|
||||
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: {
|
||||
type: DataTypes.TEXT,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
|
||||
|
||||
},
|
||||
|
||||
persona_data: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
created: {
|
||||
type: DataTypes.DATE,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
importHash: {
|
||||
@ -49,28 +45,6 @@ created: {
|
||||
);
|
||||
|
||||
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, {
|
||||
as: 'owner',
|
||||
foreignKey: {
|
||||
@ -79,9 +53,6 @@ created: {
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
db.profiles.belongsTo(db.users, {
|
||||
as: 'createdBy',
|
||||
});
|
||||
@ -91,9 +62,5 @@ created: {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
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 cors = require('cors');
|
||||
const app = express();
|
||||
@ -102,6 +101,20 @@ app.use('/api/file', fileRoutes);
|
||||
app.use('/api/pexels', pexelsRoutes);
|
||||
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);
|
||||
|
||||
@ -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/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/openai',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
mockAuthMiddleware,
|
||||
openaiRoutes,
|
||||
);
|
||||
app.use(
|
||||
'/api/ai',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
mockAuthMiddleware,
|
||||
openaiRoutes,
|
||||
);
|
||||
|
||||
@ -171,4 +184,4 @@ db.sequelize.sync().then(function () {
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
module.exports = app;
|
||||
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 BaseIcon from '../BaseIcon';
|
||||
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
} 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 {hasPermission} from "../../helpers/userPermissions";
|
||||
|
||||
type Params = (id: string) => void;
|
||||
@ -20,119 +12,83 @@ type Params = (id: string) => void;
|
||||
export const loadColumns = async (
|
||||
onDelete: Params,
|
||||
entityName: string,
|
||||
|
||||
user
|
||||
|
||||
) => {
|
||||
async function callOptionsApi(entityName: string) {
|
||||
|
||||
if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return [];
|
||||
|
||||
try {
|
||||
const data = await axios(`/${entityName}/autocomplete?limit=100`);
|
||||
return data.data;
|
||||
const data = await axios(`/${entityName}/autocomplete?limit=100`);
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_PROFILES')
|
||||
|
||||
return [
|
||||
|
||||
{
|
||||
field: 'name',
|
||||
headerName: 'ProfileName',
|
||||
headerName: 'Persona Name',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
minWidth: 150,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'owner',
|
||||
headerName: 'Owner',
|
||||
field: 'occupation',
|
||||
headerName: 'Occupation',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
minWidth: 150,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
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) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
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',
|
||||
headerName: 'Description',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
headerName: 'Summary',
|
||||
flex: 1.5,
|
||||
minWidth: 200,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
|
||||
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',
|
||||
type: 'actions',
|
||||
minWidth: 30,
|
||||
minWidth: 50,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
getActions: (params: GridRowParams) => {
|
||||
|
||||
return [
|
||||
<div key={params?.row?.id}>
|
||||
<ListActionsPopover
|
||||
onDelete={onDelete}
|
||||
itemId={params?.row?.id}
|
||||
pathEdit={`/profiles/profiles-edit/?id=${params?.row?.id}`}
|
||||
pathView={`/profiles/profiles-view/?id=${params?.row?.id}`}
|
||||
|
||||
hasUpdatePermission={hasUpdatePermission}
|
||||
|
||||
/>
|
||||
onDelete={onDelete}
|
||||
itemId={params?.row?.id}
|
||||
pathEdit={`/profiles/profiles-edit/?id=${params?.row?.id}`}
|
||||
pathView={`/profiles/profiles-view/?id=${params?.row?.id}`}
|
||||
hasUpdatePermission={hasUpdatePermission}
|
||||
/>
|
||||
</div>,
|
||||
]
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
};
|
||||
@ -9,6 +9,8 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
|
||||
import BaseIcon from "../components/BaseIcon";
|
||||
import { getPageTitle } from '../config'
|
||||
import Link from "next/link";
|
||||
import CardBox from '../components/CardBox';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
|
||||
import { hasPermission } from "../helpers/userPermissions";
|
||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
||||
@ -16,6 +18,7 @@ import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
||||
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||
|
||||
const Dashboard = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
||||
@ -24,43 +27,30 @@ const Dashboard = () => {
|
||||
|
||||
const loadingMessage = 'Loading...';
|
||||
|
||||
|
||||
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 [api_keys, setApi_keys] = React.useState(loadingMessage);
|
||||
const [extensions, setExtensions] = 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 [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({
|
||||
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() {
|
||||
const entities = ['users','roles','permissions','ai_models','api_keys','extensions','surveys','fill_sessions','request_queue','connection_logs','profiles','settings',];
|
||||
const fns = [setUsers,setRoles,setPermissions,setAi_models,setApi_keys,setExtensions,setSurveys,setFill_sessions,setRequest_queue,setConnection_logs,setProfiles,setSettings,];
|
||||
const entities = ['users','ai_models','surveys','profiles'];
|
||||
const fns = [setUsers,setAi_models,setSurveys,setProfiles];
|
||||
|
||||
const requests = entities.map((entity, index) => {
|
||||
|
||||
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
|
||||
return axios.get(`/${entity.toLowerCase()}/count`);
|
||||
} else {
|
||||
fns[index](null);
|
||||
return Promise.resolve({data: {count: null}});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Promise.allSettled(requests).then((results) => {
|
||||
@ -74,9 +64,6 @@ const Dashboard = () => {
|
||||
});
|
||||
}
|
||||
|
||||
async function getWidgets(roleId) {
|
||||
await dispatch(fetchWidgets(roleId));
|
||||
}
|
||||
React.useEffect(() => {
|
||||
if (!currentUser) return;
|
||||
loadData().then();
|
||||
@ -85,406 +72,137 @@ const Dashboard = () => {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentUser || !widgetsRole?.role?.value) return;
|
||||
getWidgets(widgetsRole?.role?.value || '').then();
|
||||
}, [widgetsRole?.role?.value]);
|
||||
dispatch(fetchWidgets(widgetsRole?.role?.value || '')).then();
|
||||
}, [widgetsRole?.role?.value, dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
{getPageTitle('Overview')}
|
||||
</title>
|
||||
<title>{getPageTitle('Dashboard')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={icon.mdiChartTimelineVariant}
|
||||
title='Overview'
|
||||
icon={icon.mdiViewDashboard}
|
||||
title='Smart Filler Dashboard'
|
||||
main>
|
||||
{''}
|
||||
</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
|
||||
currentUser={currentUser}
|
||||
isFetchingQuery={isFetchingQuery}
|
||||
setWidgetsRole={setWidgetsRole}
|
||||
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'>
|
||||
{(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`}>
|
||||
<BaseIcon
|
||||
className={`${iconsColor} animate-spin mr-5`}
|
||||
w='w-16'
|
||||
h='h-16'
|
||||
size={48}
|
||||
path={icon.mdiLoading}
|
||||
/>{' '}
|
||||
Loading widgets...
|
||||
<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 className={`${iconsColor} animate-spin mr-5`} w='w-16' h='h-16' size={48} path={icon.mdiLoading} />
|
||||
Loading...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ rolesWidgets &&
|
||||
rolesWidgets.map((widget) => (
|
||||
<SmartWidget
|
||||
key={widget.id}
|
||||
userId={currentUser?.id}
|
||||
widget={widget}
|
||||
roleId={widgetsRole?.role?.value || ''}
|
||||
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
||||
/>
|
||||
{ rolesWidgets && rolesWidgets.map((widget) => (
|
||||
<SmartWidget
|
||||
key={widget.id}
|
||||
userId={currentUser?.id}
|
||||
widget={widget}
|
||||
roleId={widgetsRole?.role?.value || ''}
|
||||
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
@ -494,4 +212,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||
}
|
||||
|
||||
export default Dashboard
|
||||
export default Dashboard
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
@ -7,13 +6,12 @@ import BaseButton from '../components/BaseButton';
|
||||
import CardBox from '../components/CardBox';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { getPageTitle } from '../config';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||
|
||||
import * as icon from '@mdi/js';
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
|
||||
export default function Starter() {
|
||||
const [illustrationImage, setIllustrationImage] = useState({
|
||||
@ -26,7 +24,7 @@ export default function Starter() {
|
||||
const [contentPosition, setContentPosition] = useState('left');
|
||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||
|
||||
const title = 'AI App Draft'
|
||||
const title = 'Smart Survey Filler AI'
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect(() => {
|
||||
@ -39,37 +37,10 @@ export default function Starter() {
|
||||
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) => {
|
||||
if (video?.video_files?.length > 0) {
|
||||
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
|
||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||
autoPlay
|
||||
@ -77,11 +48,11 @@ export default function Starter() {
|
||||
muted
|
||||
>
|
||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
||||
Your browser does not support the video tag.
|
||||
</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
|
||||
className='text-[8px]'
|
||||
className='text-[10px] text-white opacity-60 hover:opacity-100'
|
||||
href={video?.user?.url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
@ -94,66 +65,81 @@ export default function Starter() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
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',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div className="bg-slate-900 min-h-screen text-slate-100">
|
||||
<Head>
|
||||
<title>{getPageTitle('Starter Page')}</title>
|
||||
<title>{getPageTitle('Smart Survey Filler AI')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div
|
||||
className={`flex ${
|
||||
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">
|
||||
<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>
|
||||
<p className='text-center '>For guides and documentation please check
|
||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
||||
<SectionFullScreen bg='slate'>
|
||||
<div className={`flex flex-row min-h-screen w-full`}>
|
||||
{videoBlock(illustrationVideo)}
|
||||
|
||||
<div className='flex items-center justify-center flex-col space-y-8 w-full px-6 lg:px-12 py-12'>
|
||||
<div className="text-center space-y-4 max-w-2xl">
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
href='/login'
|
||||
label='Login'
|
||||
color='info'
|
||||
className='w-full'
|
||||
/>
|
||||
|
||||
</BaseButtons>
|
||||
</CardBox>
|
||||
</div>
|
||||
<CardBox className='w-full md:w-4/5 lg:w-3/4 bg-slate-800/50 border-slate-700 backdrop-blur-md shadow-2xl'>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 p-4">
|
||||
<div className="flex flex-col items-center text-center space-y-3">
|
||||
<div className="p-3 bg-blue-500/20 rounded-xl text-blue-400">
|
||||
<BaseIcon path={icon.mdiRobot} size={32} />
|
||||
</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>
|
||||
|
||||
<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">
|
||||
<BaseButton
|
||||
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>
|
||||
</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>
|
||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
|
||||
<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'>
|
||||
<p className='text-sm'>© 2026 <span className="text-slate-300 font-medium">{title}</span>. Built with AI for the future.</p>
|
||||
<div className="flex space-x-6 justify-center mt-4 md:mt-0 md:ml-8">
|
||||
<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>
|
||||
@ -162,5 +148,4 @@ export default function Starter() {
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
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 React, { ReactElement, useEffect, useState } from 'react'
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import CardBox from '../../components/CardBox'
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||
@ -16,166 +14,51 @@ import FormField from '../../components/FormField'
|
||||
import BaseDivider from '../../components/BaseDivider'
|
||||
import BaseButtons from '../../components/BaseButtons'
|
||||
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 { SelectFieldMany } from "../../components/SelectFieldMany";
|
||||
import { SwitchField } from '../../components/SwitchField'
|
||||
import {RichTextField} from "../../components/RichTextField";
|
||||
|
||||
import { update, fetch } from '../../stores/profiles/profilesSlice'
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||
import { useRouter } from 'next/router'
|
||||
import {saveFile} from "../../helpers/fileSaver";
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
import ImageField from "../../components/ImageField";
|
||||
|
||||
|
||||
|
||||
const EditProfilesPage = () => {
|
||||
const router = useRouter()
|
||||
const dispatch = useAppDispatch()
|
||||
const initVals = {
|
||||
|
||||
|
||||
'name': '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
owner: null,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
description: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
created: new Date(),
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
const [initialValues, setInitialValues] = useState(initVals)
|
||||
|
||||
const { profiles } = useAppSelector((state) => state.profiles)
|
||||
|
||||
|
||||
const initVals = {
|
||||
name: '',
|
||||
description: '',
|
||||
persona_data: {
|
||||
age: '',
|
||||
gender: '',
|
||||
occupation: '',
|
||||
interests: '',
|
||||
language: ''
|
||||
}
|
||||
}
|
||||
|
||||
const [initialValues, setInitialValues] = useState(initVals)
|
||||
const { profiles } = useAppSelector((state) => state.profiles)
|
||||
const { id } = router.query
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetch({ id: id }))
|
||||
}, [id])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof profiles === 'object') {
|
||||
setInitialValues(profiles)
|
||||
if (id) {
|
||||
dispatch(fetch({ id: id }))
|
||||
}
|
||||
}, [profiles])
|
||||
}, [id, dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof profiles === 'object') {
|
||||
const newInitialVal = {...initVals};
|
||||
Object.keys(initVals).forEach(el => newInitialVal[el] = (profiles)[el])
|
||||
setInitialValues(newInitialVal);
|
||||
if (profiles && typeof profiles === 'object') {
|
||||
const mergedValues = {
|
||||
...initVals,
|
||||
...profiles,
|
||||
persona_data: {
|
||||
...initVals.persona_data,
|
||||
...(profiles.persona_data || {})
|
||||
}
|
||||
}
|
||||
setInitialValues(mergedValues)
|
||||
}
|
||||
}, [profiles])
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
@ -186,10 +69,10 @@ const EditProfilesPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Edit profiles')}</title>
|
||||
<title>{getPageTitle('Edit Persona Profile')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit profiles'} main>
|
||||
<SectionTitleLineWithButton icon={mdiAccount} title={'Edit Persona Profile'} main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
@ -199,197 +82,50 @@ const EditProfilesPage = () => {
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<FormField label="Persona Name">
|
||||
<Field name="name" placeholder="e.g. John Doe" />
|
||||
</FormField>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<FormField label="Age">
|
||||
<Field name="persona_data.age" type="number" placeholder="Age" />
|
||||
</FormField>
|
||||
|
||||
|
||||
<FormField
|
||||
label="ProfileName"
|
||||
>
|
||||
<Field
|
||||
name="name"
|
||||
placeholder="ProfileName"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Gender">
|
||||
<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='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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='Bio / Additional Context' hasTextareaHeight>
|
||||
<Field
|
||||
name='description'
|
||||
id='description'
|
||||
component={RichTextField}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label="Submit" />
|
||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||
<BaseButton type="submit" color="info" label="Update Persona" />
|
||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/profiles/profiles-list')}/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
@ -402,14 +138,10 @@ const EditProfilesPage = () => {
|
||||
|
||||
EditProfilesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated
|
||||
|
||||
permission={'UPDATE_PROFILES'}
|
||||
|
||||
>
|
||||
<LayoutAuthenticated permission={'UPDATE_PROFILES'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditProfilesPage
|
||||
export default EditProfilesPage
|
||||
@ -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 React, { ReactElement } from 'react'
|
||||
import CardBox from '../../components/CardBox'
|
||||
@ -12,87 +12,24 @@ import FormField from '../../components/FormField'
|
||||
import BaseDivider from '../../components/BaseDivider'
|
||||
import BaseButtons from '../../components/BaseButtons'
|
||||
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 { SelectFieldMany } from "../../components/SelectFieldMany";
|
||||
import {RichTextField} from "../../components/RichTextField";
|
||||
|
||||
import { create } from '../../stores/profiles/profilesSlice'
|
||||
import { useAppDispatch } from '../../stores/hooks'
|
||||
import { useRouter } from 'next/router'
|
||||
import moment from 'moment';
|
||||
|
||||
const initialValues = {
|
||||
|
||||
|
||||
name: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
owner: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
description: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
created: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
persona_data: {
|
||||
age: '',
|
||||
gender: '',
|
||||
occupation: '',
|
||||
interests: '',
|
||||
language: 'English'
|
||||
},
|
||||
created: new Date().toISOString().slice(0, 16)
|
||||
}
|
||||
|
||||
|
||||
@ -100,172 +37,71 @@ const ProfilesNew = () => {
|
||||
const router = useRouter()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
|
||||
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
await dispatch(create(data))
|
||||
await router.push('/profiles/profiles-list')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('New Item')}</title>
|
||||
<title>{getPageTitle('New Persona Profile')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
|
||||
<SectionTitleLineWithButton icon={mdiAccount} title="New Persona Profile" main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
initialValues={
|
||||
|
||||
initialValues
|
||||
|
||||
}
|
||||
initialValues={initialValues}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="ProfileName"
|
||||
>
|
||||
<Field
|
||||
name="name"
|
||||
placeholder="ProfileName"
|
||||
/>
|
||||
</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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Persona Name" help="Give this persona a name (e.g. 'Tech Savvy Student')">
|
||||
<Field name="name" placeholder="e.g. John Doe" />
|
||||
</FormField>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<FormField label="Age">
|
||||
<Field name="persona_data.age" type="number" placeholder="Age" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Gender">
|
||||
<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>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label="Submit" />
|
||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||
<BaseButton type="submit" color="info" label="Create Persona" />
|
||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/profiles/profiles-list')}/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
@ -278,14 +114,10 @@ const ProfilesNew = () => {
|
||||
|
||||
ProfilesNew.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated
|
||||
|
||||
permission={'CREATE_PROFILES'}
|
||||
|
||||
>
|
||||
<LayoutAuthenticated permission={'CREATE_PROFILES'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfilesNew
|
||||
export default ProfilesNew
|
||||
Loading…
x
Reference in New Issue
Block a user