Auto commit: 2026-01-25T23:01:49.649Z

This commit is contained in:
Flatlogic Bot 2026-01-25 23:01:49 +00:00
parent 0bf51e6e64
commit dad06a9ce6
18 changed files with 968 additions and 1177 deletions

View File

@ -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',
}
};
};

View File

@ -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');
}
};

View File

@ -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;
};
};

View 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, {});
}
};

View File

@ -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

Binary file not shown.

View 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">&times;</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';
}
});
})();

View 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"
}
}

View 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
View 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;
}
});
}

View 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;
}
});

Binary file not shown.

Binary file not shown.

View File

@ -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>,
]
},
},
];
};
};

View File

@ -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

View File

@ -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>;
};
};

View File

@ -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

View File

@ -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