Autosave: 20260512-175841

This commit is contained in:
Flatlogic Bot 2026-05-12 17:58:39 +00:00
parent 4bc6ce8ad7
commit 498f197b6e
164 changed files with 5827 additions and 3926 deletions

View File

@ -803,6 +803,7 @@ module.exports = class UsersDBApi {
{
email: data.email,
firstName: data.firstName,
username: data.username || null,
authenticationUid: data.authenticationUid,
password: data.password,

View File

@ -0,0 +1,126 @@
module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
const table = await queryInterface.describeTable('users');
if (!table.username) {
await queryInterface.addColumn(
'users',
'username',
{
type: Sequelize.DataTypes.TEXT,
allowNull: true,
},
{ transaction },
);
}
if (!table.bio) {
await queryInterface.addColumn(
'users',
'bio',
{
type: Sequelize.DataTypes.TEXT,
allowNull: true,
},
{ transaction },
);
}
if (!table.birth_date) {
await queryInterface.addColumn(
'users',
'birth_date',
{
type: Sequelize.DataTypes.DATE,
allowNull: true,
},
{ transaction },
);
}
if (!table.account_visibility) {
await queryInterface.addColumn(
'users',
'account_visibility',
{
type: Sequelize.DataTypes.ENUM('open', 'closed'),
allowNull: false,
defaultValue: 'open',
},
{ transaction },
);
}
if (!table.nsfw_enabled) {
await queryInterface.addColumn(
'users',
'nsfw_enabled',
{
type: Sequelize.DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
{ transaction },
);
}
if (!table.nsfw_preference) {
await queryInterface.addColumn(
'users',
'nsfw_preference',
{
type: Sequelize.DataTypes.ENUM('detailed', 'fade_to_black'),
allowNull: false,
defaultValue: 'fade_to_black',
},
{ transaction },
);
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
},
async down(queryInterface) {
const transaction = await queryInterface.sequelize.transaction();
try {
const table = await queryInterface.describeTable('users');
if (table.nsfw_preference) {
await queryInterface.removeColumn('users', 'nsfw_preference', { transaction });
}
if (table.nsfw_enabled) {
await queryInterface.removeColumn('users', 'nsfw_enabled', { transaction });
}
if (table.account_visibility) {
await queryInterface.removeColumn('users', 'account_visibility', { transaction });
}
if (table.birth_date) {
await queryInterface.removeColumn('users', 'birth_date', { transaction });
}
if (table.bio) {
await queryInterface.removeColumn('users', 'bio', { transaction });
}
if (table.username) {
await queryInterface.removeColumn('users', 'username', { transaction });
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
},
};

View File

@ -2,7 +2,6 @@ const config = require('../../config');
const providers = config.providers;
const crypto = require('crypto');
const bcrypt = require('bcrypt');
const moment = require('moment');
module.exports = function(sequelize, DataTypes) {
const users = sequelize.define(
@ -14,94 +13,88 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
firstName: {
firstName: {
type: DataTypes.TEXT,
},
lastName: {
lastName: {
type: DataTypes.TEXT,
},
phoneNumber: {
phoneNumber: {
type: DataTypes.TEXT,
},
email: {
email: {
type: DataTypes.TEXT,
},
disabled: {
username: {
type: DataTypes.TEXT,
},
bio: {
type: DataTypes.TEXT,
},
birth_date: {
type: DataTypes.DATE,
},
account_visibility: {
type: DataTypes.ENUM,
allowNull: false,
defaultValue: 'open',
values: ['open', 'closed'],
},
nsfw_enabled: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
password: {
type: DataTypes.TEXT,
nsfw_preference: {
type: DataTypes.ENUM,
allowNull: false,
defaultValue: 'fade_to_black',
values: ['detailed', 'fade_to_black'],
},
emailVerified: {
disabled: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
emailVerificationToken: {
password: {
type: DataTypes.TEXT,
},
emailVerificationTokenExpiresAt: {
emailVerified: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
emailVerificationToken: {
type: DataTypes.TEXT,
},
emailVerificationTokenExpiresAt: {
type: DataTypes.DATE,
},
passwordResetToken: {
passwordResetToken: {
type: DataTypes.TEXT,
},
passwordResetTokenExpiresAt: {
passwordResetTokenExpiresAt: {
type: DataTypes.DATE,
},
provider: {
provider: {
type: DataTypes.TEXT,
},
importHash: {
@ -118,7 +111,6 @@ provider: {
);
users.associate = (db) => {
db.users.belongsToMany(db.permissions, {
as: 'custom_permissions',
foreignKey: {
@ -137,57 +129,38 @@ provider: {
through: 'usersCustom_permissionsPermissions',
});
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
db.users.hasMany(db.personas, {
as: 'personas_user',
foreignKey: {
name: 'userId',
name: 'userId',
},
constraints: false,
});
db.users.hasMany(db.bots, {
as: 'bots_author',
foreignKey: {
name: 'authorId',
name: 'authorId',
},
constraints: false,
});
db.users.hasMany(db.conversations, {
as: 'conversations_user',
foreignKey: {
name: 'userId',
name: 'userId',
},
constraints: false,
});
db.users.hasMany(db.user_search_logs, {
as: 'user_search_logs_user',
foreignKey: {
name: 'userId',
name: 'userId',
},
constraints: false,
});
//end loop
db.users.belongsTo(db.roles, {
as: 'app_role',
foreignKey: {
@ -196,8 +169,6 @@ provider: {
constraints: false,
});
db.users.hasMany(db.file, {
as: 'avatar',
foreignKey: 'belongsToId',
@ -208,7 +179,6 @@ provider: {
},
});
db.users.belongsTo(db.users, {
as: 'createdBy',
});
@ -218,48 +188,33 @@ provider: {
});
};
users.beforeCreate((record) => {
trimStringFields(record);
users.beforeCreate((users, options) => {
users = trimStringFields(users);
if (record.provider !== providers.LOCAL && Object.values(providers).indexOf(record.provider) > -1) {
record.emailVerified = true;
if (users.provider !== providers.LOCAL && Object.values(providers).indexOf(users.provider) > -1) {
users.emailVerified = true;
if (!users.password) {
const password = crypto
.randomBytes(20)
.toString('hex');
const hashedPassword = bcrypt.hashSync(
password,
config.bcrypt.saltRounds,
);
users.password = hashedPassword
}
}
});
users.beforeUpdate((users, options) => {
users = trimStringFields(users);
if (!record.password) {
const password = crypto.randomBytes(20).toString('hex');
const hashedPassword = bcrypt.hashSync(password, config.bcrypt.saltRounds);
record.password = hashedPassword;
}
}
});
users.beforeUpdate((record) => {
trimStringFields(record);
});
return users;
};
function trimStringFields(record) {
record.email = record.email ? record.email.trim() : null;
record.firstName = record.firstName ? record.firstName.trim() : null;
record.lastName = record.lastName ? record.lastName.trim() : null;
record.username = record.username ? record.username.trim().toLowerCase() : null;
record.bio = record.bio ? record.bio.trim() : null;
function trimStringFields(users) {
users.email = users.email.trim();
users.firstName = users.firstName
? users.firstName.trim()
: null;
users.lastName = users.lastName
? users.lastName.trim()
: null;
return users;
return record;
}

View File

@ -85,11 +85,8 @@ router.use(checkCrudPermissions('bots'));
* description: Some server error
*/
router.post('/', wrapAsync(async (req, res) => {
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
await BotsService.create(req.body.data, req.currentUser, true, link.host);
const payload = true;
res.status(200).send(payload);
const createdBot = await BotsService.create(req.body.data, req.currentUser);
res.status(200).send({ id: createdBot.id });
}));
/**

View File

@ -4,7 +4,7 @@ const wrapAsync = require('../helpers').wrapAsync;
const router = express.Router();
const sjs = require('sequelize-json-schema');
const { getWidget, askGpt } = require('../services/openai');
const { LocalAIApi } = require('../ai/LocalAIApi');
const AIRoleplayService = require('../services/aiRoleplay');
const loadRolesModules = () => {
try {
@ -246,19 +246,14 @@ router.post(
router.post(
'/response',
wrapAsync(async (req, res) => {
const body = req.body || {};
const options = body.options || {};
const payload = { ...body };
delete payload.options;
const response = await LocalAIApi.createResponse(payload, options);
const response = await AIRoleplayService.createResponse(req.body || {}, req.currentUser);
if (response.success) {
return res.status(200).send(response);
}
console.error('AI proxy error:', response);
const status = response.error === 'input_missing' ? 400 : 502;
const status = response.status || (response.error === 'input_missing' ? 400 : 502);
return res.status(status).send(response);
}),
);
@ -310,7 +305,7 @@ router.post(
if (!prompt) {
return res.status(400).send({
success: false,
error: 'Prompt is required',
error: 'Нужно передать prompt',
});
}

View File

@ -87,11 +87,8 @@ router.use(checkCrudPermissions('personas'));
* description: Some server error
*/
router.post('/', wrapAsync(async (req, res) => {
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
await PersonasService.create(req.body.data, req.currentUser, true, link.host);
const payload = true;
res.status(200).send(payload);
const createdPersona = await PersonasService.create(req.body.data, req.currentUser);
res.status(200).send({ id: createdPersona.id });
}));
/**

View File

@ -44,8 +44,8 @@ router.post('/', async (req, res) => {
const foundMatches = await SearchService.search(searchQuery, req.currentUser );
res.json(foundMatches);
} catch (error) {
console.error('Internal Server Error', error);
res.status(500).json({ error: 'Internal Server Error' });
console.error('Внутренняя ошибка сервера', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});

View File

@ -0,0 +1,521 @@
const db = require('../db/models');
const { LocalAIApi } = require('../ai/LocalAIApi');
const DEFAULT_ROLEPLAY_MODEL = 'venice/uncensored';
const DEFAULT_PROXY_MODEL = process.env.AI_DEFAULT_MODEL || 'gpt-5-mini';
const DEFAULT_PROXY_MODEL_LABEL = 'proxy-configured-default';
const MAX_HISTORY_MESSAGES = 20;
function buildError(status, message, extra = {}) {
return {
success: false,
status,
error: extra.error || message,
message,
...extra,
};
}
function buildLoggedPayload(payload = {}) {
const loggedPayload = { ...(payload || {}) };
if (!loggedPayload.model) {
loggedPayload.model = DEFAULT_PROXY_MODEL;
}
if (!loggedPayload.project_uuid && process.env.PROJECT_UUID) {
loggedPayload.project_uuid = process.env.PROJECT_UUID;
}
return loggedPayload;
}
function isProxyBadRequest(response = {}) {
const values = [
response.status,
response.error,
response.message,
response?.data?.status,
response?.data?.error,
response?.data?.message,
]
.filter((value) => value !== undefined && value !== null)
.map((value) => String(value).toLowerCase());
return values.some((value) => value.includes('status 400') || value === '400' || value.includes('bad request'));
}
function logAiProxyFailure(label, payload, options, response) {
console.error(label, {
payload: buildLoggedPayload(payload),
options,
response,
});
}
function attachModelMeta(response, meta = {}) {
if (!response || typeof response !== 'object' || !response.success) {
return response;
}
response.meta = {
...(response.meta || {}),
...meta,
};
return response;
}
function normalizeText(value) {
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed || '';
}
if (Array.isArray(value)) {
const joined = value
.map((item) => {
if (typeof item === 'string') {
return item;
}
if (item && typeof item === 'object') {
if (typeof item.text === 'string') {
return item.text;
}
if (typeof item.content === 'string') {
return item.content;
}
}
return '';
})
.filter(Boolean)
.join('\n');
return joined.trim();
}
if (value && typeof value === 'object') {
if (typeof value.text === 'string') {
return value.text.trim();
}
if (typeof value.content === 'string') {
return value.content.trim();
}
}
return '';
}
function extractLatestUserMessage(input) {
if (!Array.isArray(input)) {
return '';
}
for (let index = input.length - 1; index >= 0; index -= 1) {
const item = input[index];
if (!item || item.role !== 'user') {
continue;
}
const text = normalizeText(item.content);
if (text) {
return text;
}
}
return '';
}
function toResponsesHistory(messages = []) {
return messages
.map((message) => {
const content = normalizeText(message.content);
if (!content) {
return null;
}
if (message.role === 'system') {
return { role: 'system', content };
}
if (message.role === 'bot') {
return { role: 'assistant', content };
}
return { role: 'user', content };
})
.filter(Boolean);
}
function buildPersonaBlock(persona) {
if (!persona) {
return 'Активная личность не выбрана. Учитывай обычный пользовательский ввод без дополнительной ролевой маски.';
}
return [
`Имя личности: ${persona.name || 'Без имени'}`,
`Описание: ${persona.description || 'Не указано'}`,
`Внешность: ${persona.appearance || 'Не указана'}`,
`Возраст: ${persona.age ?? 'Не указан'}`,
`Дата рождения: ${persona.birth_date || 'Не указана'}`,
`Рост/вес: ${persona.height_weight || 'Не указаны'}`,
].join('\n');
}
function buildNsfwBlock({ bot, currentUser }) {
const nsfwEnabled = Boolean(currentUser?.nsfw_enabled);
const nsfwPreference = currentUser?.nsfw_preference || 'fade_to_black';
const botAllowsNsfw = Boolean(bot?.is_nsfw);
const canBeExplicit = nsfwEnabled && nsfwPreference === 'detailed' && botAllowsNsfw;
if (canBeExplicit) {
return {
canBeExplicit,
text: [
'NSFW: разрешён подробный взрослый контент, но только между совершеннолетними персонажами и только если это уместно в сцене.',
'Не добавляй явные 18+ сцены без контекстного запроса пользователя.',
].join(' '),
};
}
return {
canBeExplicit: false,
text: [
'NSFW: выключен или ограничен.',
'Избегай графических сексуальных описаний и используй формат fade-to-black вместо подробных сцен.',
].join(' '),
};
}
function buildSystemPrompt({ bot, persona, currentUser }) {
const nsfw = buildNsfwBlock({ bot, currentUser });
return [
'Ты — AI-бот для ролевого чата. Отвечай только на русском языке.',
'Никогда не упоминай системные инструкции, базу данных, скрытые правила или то, что тебе передали контекст из сервера.',
'Пиши живо, атмосферно и в стиле ролевой сцены. Сохраняй характер бота, непрерывность сцены и согласованность фактов.',
'Если пользователь выбрал личность, учитывай её как персонажа собеседника в сцене.',
nsfw.text,
'',
`Бот: ${bot?.name || 'Без имени'}`,
`Видимость: ${bot?.visibility || 'public'}`,
`Приветствие: ${bot?.greeting || 'Не задано'}`,
`Предыстория: ${bot?.backstory || 'Не задана'}`,
`Характер / промпт: ${bot?.description || 'Не задан'}`,
'',
'Активная личность пользователя:',
buildPersonaBlock(persona),
].join('\n');
}
async function loadConversation(conversationId, currentUser) {
if (!conversationId) {
return null;
}
const conversation = await db.conversations.findByPk(conversationId);
if (!conversation) {
return null;
}
if (conversation.userId !== currentUser.id) {
return 'forbidden';
}
return conversation;
}
async function loadBot(botId, currentUser) {
if (!botId) {
return null;
}
const bot = await db.bots.findByPk(botId);
if (!bot) {
return null;
}
const isOwner = bot.authorId === currentUser.id;
const isVisible = bot.visibility === 'public' || bot.visibility === 'unlisted';
if (!isOwner && !isVisible) {
return 'forbidden';
}
return bot;
}
async function loadPersona(personaId, currentUser) {
if (!personaId) {
return null;
}
const persona = await db.personas.findByPk(personaId);
if (!persona) {
return null;
}
if (persona.userId !== currentUser.id) {
return 'forbidden';
}
return persona;
}
async function loadActivePersona(currentUser) {
return db.personas.findOne({
where: {
userId: currentUser.id,
is_active: true,
},
order: [['updatedAt', 'desc']],
});
}
async function loadHistory(conversationId) {
if (!conversationId) {
return [];
}
const rows = await db.messages.findAll({
where: {
conversationId,
},
order: [['createdAt', 'desc']],
limit: MAX_HISTORY_MESSAGES,
});
return rows.reverse();
}
async function ensureConversation({ conversation, bot, persona, currentUser, canBeExplicit }) {
if (conversation) {
return conversation;
}
const now = new Date();
return db.conversations.create({
title: `${bot.name || 'Бот'} · ${persona?.name || 'Диалог'}`,
status: 'active',
started_at: now,
last_message_at: now,
is_nsfw: canBeExplicit,
userId: currentUser.id,
botId: bot.id,
personaId: persona?.id || null,
createdById: currentUser.id,
updatedById: currentUser.id,
});
}
async function saveTurn({ conversation, userMessage, assistantMessage, currentUser, canBeExplicit, persona }) {
const now = new Date();
await db.messages.create({
role: 'user',
content: userMessage,
sent_at: now,
token_count: null,
conversationId: conversation.id,
createdById: currentUser.id,
updatedById: currentUser.id,
});
if (assistantMessage) {
await db.messages.create({
role: 'bot',
content: assistantMessage,
sent_at: new Date(),
token_count: null,
conversationId: conversation.id,
createdById: currentUser.id,
updatedById: currentUser.id,
});
}
await conversation.update({
last_message_at: new Date(),
is_nsfw: canBeExplicit,
personaId: persona?.id || null,
updatedById: currentUser.id,
});
}
async function requestAiWithFallback(payload = {}, options = {}) {
const primaryPayload = { ...(payload || {}) };
const primaryResponse = await LocalAIApi.createResponse(primaryPayload, options);
if (primaryResponse.success) {
return attachModelMeta(primaryResponse, {
requestedModel: primaryPayload.model || null,
resolvedModel: primaryPayload.model || DEFAULT_PROXY_MODEL,
usedModelFallback: false,
});
}
logAiProxyFailure('AI proxy request failed:', primaryPayload, options, primaryResponse);
const shouldRetryWithConfiguredDefault =
primaryPayload.model === DEFAULT_ROLEPLAY_MODEL && isProxyBadRequest(primaryResponse);
if (!shouldRetryWithConfiguredDefault) {
return primaryResponse;
}
const fallbackPayload = { ...(payload || {}) };
delete fallbackPayload.model;
console.error('Retrying AI request with configured default model after venice/uncensored rejection.', {
requestedModel: primaryPayload.model,
fallbackModel: DEFAULT_PROXY_MODEL,
});
const fallbackResponse = await LocalAIApi.createResponse(fallbackPayload, options);
if (!fallbackResponse.success) {
logAiProxyFailure('AI proxy fallback request failed:', fallbackPayload, options, fallbackResponse);
return fallbackResponse;
}
return attachModelMeta(fallbackResponse, {
requestedModel: primaryPayload.model,
resolvedModel: DEFAULT_PROXY_MODEL_LABEL,
fallbackModel: DEFAULT_PROXY_MODEL,
usedModelFallback: true,
});
}
module.exports = class AIRoleplayService {
static async createResponse(body = {}, currentUser) {
try {
const options = body.options || {};
const payload = { ...body };
delete payload.options;
const contextRequested = Boolean(
body.botId || body.personaId || body.conversationId || body.userMessage || body.contextMode === 'roleplay',
);
if (!payload.model) {
payload.model = DEFAULT_ROLEPLAY_MODEL;
}
if (!contextRequested) {
return requestAiWithFallback(payload, options);
}
if (!currentUser?.id) {
return buildError(403, 'Требуется авторизация для AI-чата.');
}
let conversation = await loadConversation(body.conversationId, currentUser);
if (conversation === 'forbidden') {
return buildError(403, 'У вас нет доступа к этому диалогу.');
}
const resolvedBotId = body.botId || conversation?.botId;
if (!resolvedBotId) {
return buildError(400, 'Не выбран бот для AI-ответа.');
}
const bot = await loadBot(resolvedBotId, currentUser);
if (bot === 'forbidden') {
return buildError(403, 'У вас нет доступа к этому боту.');
}
if (!bot) {
return buildError(404, 'Бот не найден.');
}
const resolvedPersonaId = body.personaId || conversation?.personaId;
let persona = await loadPersona(resolvedPersonaId, currentUser);
if (persona === 'forbidden') {
return buildError(403, 'У вас нет доступа к этой личности.');
}
if (!persona && !resolvedPersonaId) {
persona = await loadActivePersona(currentUser);
}
const userMessage = normalizeText(body.userMessage) || extractLatestUserMessage(payload.input);
if (!userMessage) {
return buildError(400, 'Сообщение пользователя пустое.');
}
const history = conversation ? await loadHistory(conversation.id) : [];
const nsfw = buildNsfwBlock({ bot, currentUser });
const systemPrompt = buildSystemPrompt({ bot, persona, currentUser });
payload.input = [
{ role: 'system', content: systemPrompt },
...toResponsesHistory(history),
{ role: 'user', content: userMessage },
];
delete payload.botId;
delete payload.personaId;
delete payload.conversationId;
delete payload.userMessage;
delete payload.saveConversation;
delete payload.contextMode;
const response = await requestAiWithFallback(payload, options);
if (!response.success) {
return {
...response,
status: response.status || (response.error === 'input_missing' ? 400 : 502),
};
}
let assistantMessage = LocalAIApi.extractText(response);
if (!assistantMessage) {
try {
const decoded = LocalAIApi.decodeJsonFromResponse(response);
assistantMessage = JSON.stringify(decoded);
} catch (error) {
console.error('AI JSON decode failed:', error);
assistantMessage = '';
}
}
if (body.saveConversation !== false) {
conversation = await ensureConversation({
conversation,
bot,
persona,
currentUser,
canBeExplicit: nsfw.canBeExplicit,
});
await saveTurn({
conversation,
userMessage,
assistantMessage,
currentUser,
canBeExplicit: nsfw.canBeExplicit,
persona,
});
}
response.meta = {
...(response.meta || {}),
conversationId: conversation?.id || body.conversationId || null,
botId: bot.id,
personaId: persona?.id || null,
model: response?.meta?.resolvedModel || payload.model || DEFAULT_PROXY_MODEL,
requestedModel: response?.meta?.requestedModel || payload.model || null,
resolvedModel: response?.meta?.resolvedModel || payload.model || DEFAULT_PROXY_MODEL,
fallbackModel: response?.meta?.fallbackModel || null,
usedModelFallback: Boolean(response?.meta?.usedModelFallback),
nsfw: nsfw.canBeExplicit,
};
return response;
} catch (error) {
console.error('Roleplay AI service failed:', error);
return buildError(error.code || 500, error.message || 'Не удалось получить ответ от AI.');
}
}
};

View File

@ -1,17 +1,130 @@
const UsersDBApi = require('../db/api/users');
const FileDBApi = require('../db/api/file');
const ValidationError = require('./notifications/errors/validation');
const ForbiddenError = require('./notifications/errors/forbidden');
const bcrypt = require('bcrypt');
const EmailAddressVerificationEmail = require('./email/list/addressVerification');
const InvitationEmail = require("./email/list/invitation");
const InvitationEmail = require('./email/list/invitation');
const PasswordResetEmail = require('./email/list/passwordReset');
const EmailSender = require('./email');
const config = require('../config');
const helpers = require('../helpers');
const db = require('../db/models');
const { validateUniversalAlphabetName } = require('./nameValidation');
const { Op } = db.Sequelize;
const USERNAME_REGEX = /^[A-Za-z0-9_]{3,30}$/;
const ACCOUNT_VISIBILITY = new Set(['open', 'closed']);
const NSFW_PREFERENCES = new Set(['detailed', 'fade_to_black']);
function buildValidationError(message) {
const error = new Error(message);
error.code = 400;
return error;
}
function sanitizeOptionalText(value) {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
if (typeof value !== 'string') {
return value;
}
const trimmed = value.trim();
return trimmed || null;
}
function sanitizeProfilePayload(data = {}) {
const payload = {};
if (data.firstName !== undefined) payload.firstName = sanitizeOptionalText(data.firstName);
if (data.lastName !== undefined) payload.lastName = sanitizeOptionalText(data.lastName);
if (data.phoneNumber !== undefined) payload.phoneNumber = sanitizeOptionalText(data.phoneNumber);
if (data.username !== undefined) {
payload.username = sanitizeOptionalText(data.username);
if (typeof payload.username === 'string') {
payload.username = payload.username.toLowerCase();
}
}
if (data.bio !== undefined) payload.bio = sanitizeOptionalText(data.bio);
if (data.account_visibility !== undefined) payload.account_visibility = data.account_visibility;
if (data.birth_date !== undefined) {
payload.birth_date = data.birth_date ? new Date(data.birth_date) : null;
}
if (data.nsfw_enabled !== undefined) payload.nsfw_enabled = Boolean(data.nsfw_enabled);
if (data.nsfw_preference !== undefined) payload.nsfw_preference = data.nsfw_preference;
return payload;
}
async function validateProfilePayload(data, currentUser, transaction) {
const firstNameError = validateUniversalAlphabetName(data.firstName, {
label: 'Имя профиля',
maxLength: 80,
});
if (firstNameError) {
throw buildValidationError(firstNameError);
}
const lastNameError = validateUniversalAlphabetName(data.lastName, {
label: 'Дополнение к имени',
maxLength: 80,
required: false,
});
if (lastNameError) {
throw buildValidationError(lastNameError);
}
if (data.username !== undefined && data.username !== null) {
if (!USERNAME_REGEX.test(data.username)) {
throw buildValidationError('Юзернейм должен содержать 330 символов: латиницу, цифры или подчёркивание.');
}
const existingUser = await db.users.findOne({
where: {
username: data.username,
id: {
[Op.ne]: currentUser.id,
},
},
transaction,
});
if (existingUser) {
throw buildValidationError('Этот юзернейм уже занят.');
}
}
if (typeof data.bio === 'string' && data.bio.length > 250) {
throw buildValidationError('Описание профиля должно быть не длиннее 250 символов.');
}
if (data.account_visibility !== undefined && data.account_visibility !== null && !ACCOUNT_VISIBILITY.has(data.account_visibility)) {
throw buildValidationError('Видимость аккаунта должна быть: «Открытый» или «Закрытый».');
}
if (data.birth_date !== undefined && data.birth_date !== null) {
if (Number.isNaN(new Date(data.birth_date).getTime())) {
throw buildValidationError('Дата рождения указана некорректно.');
}
}
if (data.nsfw_preference !== undefined && data.nsfw_preference !== null && !NSFW_PREFERENCES.has(data.nsfw_preference)) {
throw buildValidationError('Настройка NSFW должна быть: «Детальное описание 18+ сцен» или «Fade to black / без подробностей».');
}
}
class Auth {
static async signup(email, password, options = {}, host) {
const user = await UsersDBApi.findBy({email});
const user = await UsersDBApi.findBy({ email });
const hashedPassword = await bcrypt.hash(
password,
@ -47,19 +160,21 @@ class Auth {
const data = {
user: {
id: user.id,
email: user.email
}
email: user.email,
},
};
return helpers.jwtSign(data);
}
const defaultName = email.split('@')[0];
const newUser = await UsersDBApi.createFromAuth(
{
firstName: email.split('@')[0],
firstName: defaultName,
password: hashedPassword,
email: email,
email,
username: defaultName.toLowerCase().replace(/[^a-z0-9_]/g, '_').slice(0, 30) || null,
},
options,
);
@ -74,15 +189,15 @@ class Auth {
const data = {
user: {
id: newUser.id,
email: newUser.email
}
email: newUser.email,
},
};
return helpers.jwtSign(data);
}
static async signin(email, password, options = {}) {
const user = await UsersDBApi.findBy({email});
static async signin(email, password) {
const user = await UsersDBApi.findBy({ email });
if (!user) {
throw new ValidationError(
@ -126,30 +241,21 @@ class Auth {
const data = {
user: {
id: user.id,
email: user.email
}
email: user.email,
},
};
return helpers.jwtSign(data);
}
static async sendEmailAddressVerificationEmail(
email,
host,
) {
static async sendEmailAddressVerificationEmail(email, host) {
let link;
try {
const token = await UsersDBApi.generateEmailVerificationToken(
email,
);
const token = await UsersDBApi.generateEmailVerificationToken(email);
link = `${host}/verify-email?token=${token}`;
} catch (error) {
console.error(error);
throw new ValidationError(
'auth.emailAddressVerificationEmail.error',
);
throw new ValidationError('auth.emailAddressVerificationEmail.error');
}
const emailAddressVerificationEmail = new EmailAddressVerificationEmail(
@ -157,61 +263,39 @@ class Auth {
link,
);
return new EmailSender(
emailAddressVerificationEmail,
).send();
return new EmailSender(emailAddressVerificationEmail).send();
}
static async sendPasswordResetEmail(email, type = 'register', host) {
let link;
try {
const token = await UsersDBApi.generatePasswordResetToken(
email,
);
const token = await UsersDBApi.generatePasswordResetToken(email);
link = `${host}/password-reset?token=${token}`;
} catch (error) {
console.error(error);
throw new ValidationError(
'auth.passwordReset.error',
);
throw new ValidationError('auth.passwordReset.error');
}
let passwordResetEmail;
if (type === 'register') {
passwordResetEmail = new PasswordResetEmail(
email,
link,
);
passwordResetEmail = new PasswordResetEmail(email, link);
}
if (type === 'invitation') {
passwordResetEmail = new InvitationEmail(
email,
link,
);
passwordResetEmail = new InvitationEmail(email, link);
}
return new EmailSender(passwordResetEmail).send();
}
static async verifyEmail(token, options = {}) {
const user = await UsersDBApi.findByEmailVerificationToken(
token,
options,
);
const user = await UsersDBApi.findByEmailVerificationToken(token, options);
if (!user) {
throw new ValidationError(
'auth.emailAddressVerificationEmail.invalidToken',
);
throw new ValidationError('auth.emailAddressVerificationEmail.invalidToken');
}
return UsersDBApi.markEmailVerified(
user.id,
options,
);
return UsersDBApi.markEmailVerified(user.id, options);
}
static async passwordUpdate(currentPassword, newPassword, options) {
@ -226,9 +310,7 @@ class Auth {
);
if (!currentPasswordMatch) {
throw new ValidationError(
'auth.wrongPassword'
)
throw new ValidationError('auth.wrongPassword');
}
const newPasswordMatch = await bcrypt.compare(
@ -237,9 +319,7 @@ class Auth {
);
if (newPasswordMatch) {
throw new ValidationError(
'auth.passwordUpdate.samePassword'
)
throw new ValidationError('auth.passwordUpdate.samePassword');
}
const hashedPassword = await bcrypt.hash(
@ -254,20 +334,11 @@ class Auth {
);
}
static async passwordReset(
token,
password,
options = {},
) {
const user = await UsersDBApi.findByPasswordResetToken(
token,
options,
);
static async passwordReset(token, password, options = {}) {
const user = await UsersDBApi.findByPasswordResetToken(token, options);
if (!user) {
throw new ValidationError(
'auth.passwordReset.invalidToken',
);
throw new ValidationError('auth.passwordReset.invalidToken');
}
const hashedPassword = await bcrypt.hash(
@ -283,25 +354,41 @@ class Auth {
}
static async updateProfile(data, currentUser) {
let transaction = await db.sequelize.transaction();
const transaction = await db.sequelize.transaction();
try {
await UsersDBApi.findBy(
{id: currentUser.id},
{transaction},
);
const user = await db.users.findByPk(currentUser.id, { transaction });
await UsersDBApi.update(
currentUser.id,
data,
{
currentUser,
transaction
},
);
if (!user) {
throw new ForbiddenError();
}
const payload = sanitizeProfilePayload(data);
await validateProfilePayload(payload, currentUser, transaction);
if (payload.nsfw_enabled === false) {
payload.nsfw_preference = 'fade_to_black';
}
await user.update(payload, { transaction });
if (data.avatar !== undefined) {
await FileDBApi.replaceRelationFiles(
{
belongsTo: db.users.getTableName(),
belongsToColumn: 'avatar',
belongsToId: user.id,
},
data.avatar,
{
currentUser,
transaction,
},
);
}
await transaction.commit();
return user;
} catch (error) {
await transaction.rollback();
throw error;

View File

@ -1,36 +1,100 @@
const db = require('../db/models');
const BotsDBApi = require('../db/api/bots');
const processFile = require("../middlewares/upload");
const ValidationError = require('./notifications/errors/validation');
const csv = require('csv-parser');
const axios = require('axios');
const config = require('../config');
const stream = require('stream');
const db = require('../db/models');
const BotsDBApi = require('../db/api/bots');
const processFile = require('../middlewares/upload');
const ValidationError = require('./notifications/errors/validation');
const { validateUniversalAlphabetName } = require('./nameValidation');
const BOT_VISIBILITY = new Set(['public', 'unlisted', 'private']);
function buildValidationError(message) {
const error = new Error(message);
error.code = 400;
return error;
}
function sanitizeOptionalText(value) {
if (typeof value !== 'string') {
return value;
}
const trimmed = value.trim();
return trimmed || null;
}
function sanitizeBotPayload(data = {}) {
return {
...data,
name: typeof data.name === 'string' ? data.name.trim() : data.name,
backstory: sanitizeOptionalText(data.backstory),
greeting: sanitizeOptionalText(data.greeting),
description: sanitizeOptionalText(data.description),
};
}
async function validateBotPayload(data, { isCreate = false } = {}) {
if (!data || typeof data !== 'object') {
throw buildValidationError('Требуются данные бота.');
}
if (data.name !== undefined) {
if (!data.name) {
throw buildValidationError('Имя бота обязательно.');
}
const nameError = validateUniversalAlphabetName(data.name, {
label: 'Имя бота',
maxLength: 100,
});
if (nameError) {
throw buildValidationError(nameError);
}
}
if (data.visibility !== undefined && data.visibility !== null && !BOT_VISIBILITY.has(data.visibility)) {
throw buildValidationError('Видимость бота должна быть: «Публичный», «По ссылке» или «Приватный».');
}
const requiresContent = isCreate && !data.is_draft;
if (requiresContent) {
if (!data.backstory) {
throw buildValidationError('Предыстория обязательна, если бот не сохраняется как черновик.');
}
if (!data.greeting) {
throw buildValidationError('Приветствие обязательно, если бот не сохраняется как черновик.');
}
if (!data.description) {
throw buildValidationError('Описание / промпт обязательны, если бот не сохраняется как черновик.');
}
}
}
module.exports = class BotsService {
static async create(data, currentUser) {
const transaction = await db.sequelize.transaction();
try {
await BotsDBApi.create(
data,
{
currentUser,
transaction,
},
);
const payload = sanitizeBotPayload(data);
await validateBotPayload(payload, { isCreate: true });
const createdBot = await BotsDBApi.create(payload, {
currentUser,
transaction,
});
await transaction.commit();
return createdBot;
} catch (error) {
await transaction.rollback();
throw error;
}
};
}
static async bulkImport(req, res, sendInvitationEmails = true, host) {
static async bulkImport(req, res) {
const transaction = await db.sequelize.transaction();
try {
@ -38,7 +102,7 @@ module.exports = class BotsService {
const bufferStream = new stream.PassThrough();
const results = [];
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8'));
await new Promise((resolve, reject) => {
bufferStream
@ -49,13 +113,13 @@ module.exports = class BotsService {
resolve();
})
.on('error', (error) => reject(error));
})
});
await BotsDBApi.bulkImport(results, {
transaction,
ignoreDuplicates: true,
validate: true,
currentUser: req.currentUser
transaction,
ignoreDuplicates: true,
validate: true,
currentUser: req.currentUser,
});
await transaction.commit();
@ -68,34 +132,30 @@ module.exports = class BotsService {
static async update(data, id, currentUser) {
const transaction = await db.sequelize.transaction();
try {
let bots = await BotsDBApi.findBy(
{id},
{transaction},
const bots = await BotsDBApi.findBy(
{ id },
{ transaction },
);
if (!bots) {
throw new ValidationError(
'botsNotFound',
);
throw new ValidationError('botsNotFound');
}
const updatedBots = await BotsDBApi.update(
id,
data,
{
currentUser,
transaction,
},
);
const payload = sanitizeBotPayload(data);
await validateBotPayload(payload);
const updatedBots = await BotsDBApi.update(id, payload, {
currentUser,
transaction,
});
await transaction.commit();
return updatedBots;
} catch (error) {
await transaction.rollback();
throw error;
}
};
}
static async deleteByIds(ids, currentUser) {
const transaction = await db.sequelize.transaction();
@ -117,13 +177,10 @@ module.exports = class BotsService {
const transaction = await db.sequelize.transaction();
try {
await BotsDBApi.remove(
id,
{
currentUser,
transaction,
},
);
await BotsDBApi.remove(id, {
currentUser,
transaction,
});
await transaction.commit();
} catch (error) {
@ -131,8 +188,4 @@ module.exports = class BotsService {
throw error;
}
}
};

View File

@ -0,0 +1,35 @@
const UNIVERSAL_ALPHABET_NAME_REGEX = /^[\p{L}\p{M}]+(?:[ -][\p{L}\p{M}]+)*$/u;
const UNIVERSAL_ALPHABET_NAME_HELP =
'Используй буквы любого алфавита. Между словами допустимы пробелы и дефисы.';
function validateUniversalAlphabetName(value, { label, maxLength = 100, required = true } = {}) {
if (value === undefined) {
return '';
}
const normalizedValue = typeof value === 'string' ? value.trim() : value;
if (normalizedValue === null || normalizedValue === '') {
return required ? `Укажи ${String(label || 'название').toLowerCase()}.` : '';
}
if (typeof normalizedValue !== 'string') {
return `${label} указано некорректно.`;
}
if (normalizedValue.length > maxLength) {
return `${label} должно быть не длиннее ${maxLength} символов.`;
}
if (!UNIVERSAL_ALPHABET_NAME_REGEX.test(normalizedValue)) {
return `${label} может содержать только буквы любого алфавита. Между словами допустимы пробелы и дефисы.`;
}
return '';
}
module.exports = {
UNIVERSAL_ALPHABET_NAME_HELP,
UNIVERSAL_ALPHABET_NAME_REGEX,
validateUniversalAlphabetName,
};

View File

@ -35,7 +35,7 @@ module.exports = class OpenAiService {
if (!prompt) {
return {
success: false,
error: 'Prompt is required'
error: 'Нужно передать prompt'
};
}
@ -59,7 +59,7 @@ module.exports = class OpenAiService {
console.error('AI JSON decode failed:', error);
return {
success: false,
error: 'AI response parsing failed',
error: 'Не удалось разобрать ответ AI',
details: error.message || String(error),
};
}
@ -73,7 +73,7 @@ module.exports = class OpenAiService {
console.error('AI proxy error:', response);
return {
success: false,
error: response.error || response.message || 'AI proxy error',
error: response.error || response.message || 'Ошибка AI-прокси',
response,
};
}

View File

@ -1,36 +1,106 @@
const db = require('../db/models');
const PersonasDBApi = require('../db/api/personas');
const processFile = require("../middlewares/upload");
const ValidationError = require('./notifications/errors/validation');
const csv = require('csv-parser');
const axios = require('axios');
const config = require('../config');
const stream = require('stream');
const db = require('../db/models');
const PersonasDBApi = require('../db/api/personas');
const processFile = require('../middlewares/upload');
const ValidationError = require('./notifications/errors/validation');
const { validateUniversalAlphabetName } = require('./nameValidation');
const MAX_PERSONAS_PER_USER = 10;
function buildValidationError(message) {
const error = new Error(message);
error.code = 400;
return error;
}
function sanitizeOptionalText(value) {
if (typeof value !== 'string') {
return value;
}
const trimmed = value.trim();
return trimmed || null;
}
function sanitizePersonaPayload(data = {}) {
return {
...data,
name: typeof data.name === 'string' ? data.name.trim() : data.name,
description: sanitizeOptionalText(data.description),
appearance: sanitizeOptionalText(data.appearance),
height_weight: sanitizeOptionalText(data.height_weight),
age:
data.age === '' || data.age === null || data.age === undefined
? null
: Number(data.age),
};
}
async function validatePersonaPayload(data, currentUser, { transaction, isCreate = false } = {}) {
if (!data || typeof data !== 'object') {
throw buildValidationError('Требуются данные личности.');
}
if (data.name !== undefined) {
if (!data.name) {
throw buildValidationError('Имя личности обязательно.');
}
const nameError = validateUniversalAlphabetName(data.name, {
label: 'Имя личности',
maxLength: 100,
});
if (nameError) {
throw buildValidationError(nameError);
}
}
if (data.age !== null && data.age !== undefined) {
if (!Number.isInteger(data.age) || data.age < 0 || data.age > 999) {
throw buildValidationError('Возраст личности должен быть целым числом от 0 до 999.');
}
}
const ownerId = data.user || currentUser?.id;
if (isCreate && ownerId) {
const personaCount = await db.personas.count({
where: {
userId: ownerId,
},
transaction,
});
if (personaCount >= MAX_PERSONAS_PER_USER) {
throw buildValidationError(`Можно создать не более ${MAX_PERSONAS_PER_USER} личностей.`);
}
}
}
module.exports = class PersonasService {
static async create(data, currentUser) {
const transaction = await db.sequelize.transaction();
try {
await PersonasDBApi.create(
data,
{
currentUser,
transaction,
},
);
const payload = sanitizePersonaPayload(data);
await validatePersonaPayload(payload, currentUser, { transaction, isCreate: true });
const createdPersona = await PersonasDBApi.create(payload, {
currentUser,
transaction,
});
await transaction.commit();
return createdPersona;
} catch (error) {
await transaction.rollback();
throw error;
}
};
}
static async bulkImport(req, res, sendInvitationEmails = true, host) {
static async bulkImport(req, res) {
const transaction = await db.sequelize.transaction();
try {
@ -38,7 +108,7 @@ module.exports = class PersonasService {
const bufferStream = new stream.PassThrough();
const results = [];
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8'));
await new Promise((resolve, reject) => {
bufferStream
@ -49,13 +119,13 @@ module.exports = class PersonasService {
resolve();
})
.on('error', (error) => reject(error));
})
});
await PersonasDBApi.bulkImport(results, {
transaction,
ignoreDuplicates: true,
validate: true,
currentUser: req.currentUser
transaction,
ignoreDuplicates: true,
validate: true,
currentUser: req.currentUser,
});
await transaction.commit();
@ -68,34 +138,30 @@ module.exports = class PersonasService {
static async update(data, id, currentUser) {
const transaction = await db.sequelize.transaction();
try {
let personas = await PersonasDBApi.findBy(
{id},
{transaction},
const personas = await PersonasDBApi.findBy(
{ id },
{ transaction },
);
if (!personas) {
throw new ValidationError(
'personasNotFound',
);
throw new ValidationError('personasNotFound');
}
const updatedPersonas = await PersonasDBApi.update(
id,
data,
{
currentUser,
transaction,
},
);
const payload = sanitizePersonaPayload(data);
await validatePersonaPayload(payload, currentUser, { transaction });
const updatedPersonas = await PersonasDBApi.update(id, payload, {
currentUser,
transaction,
});
await transaction.commit();
return updatedPersonas;
} catch (error) {
await transaction.rollback();
throw error;
}
};
}
static async deleteByIds(ids, currentUser) {
const transaction = await db.sequelize.transaction();
@ -117,13 +183,10 @@ module.exports = class PersonasService {
const transaction = await db.sequelize.transaction();
try {
await PersonasDBApi.remove(
id,
{
currentUser,
transaction,
},
);
await PersonasDBApi.remove(id, {
currentUser,
transaction,
});
await transaction.commit();
} catch (error) {
@ -131,8 +194,4 @@ module.exports = class PersonasService {
throw error;
}
}
};

View File

@ -1,27 +1,33 @@
<svg width="134" height="110" viewBox="0 0 134 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_311_30216)">
<circle cx="56.423" cy="43.0949" r="32.3527" fill="#F8F9FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.8296 52.2405C19.4251 51.6706 19.4251 50.7466 18.8296 50.1766L13.6813 45.2494L18.8296 40.3222C19.4251 39.7523 19.4251 38.8283 18.8296 38.2583C18.2341 37.6884 17.2686 37.6884 16.6731 38.2583L10.4466 44.2175C9.85113 44.7874 9.85113 45.7114 10.4466 46.2814L16.6731 52.2405C17.2686 52.8105 18.2341 52.8105 18.8296 52.2405Z" fill="#02004E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M95.1704 52.2405C94.5749 51.6706 94.5749 50.7466 95.1704 50.1766L100.319 45.2494L95.1704 40.3222C94.5749 39.7523 94.5749 38.8283 95.1704 38.2583C95.7659 37.6884 96.7314 37.6884 97.3269 38.2583L103.553 44.2175C104.149 44.7874 104.149 45.7114 103.553 46.2814L97.3269 52.2405C96.7314 52.8105 95.7659 52.8105 95.1704 52.2405Z" fill="#02004E"/>
<path d="M56.9779 79.2582C74.2516 79.2582 88.3475 66.645 89.1339 49.832C89.1722 49.0134 88.4956 48.3477 87.6654 48.3477H83.7825H79.8996C79.0695 48.3477 78.3965 49.0119 78.3965 49.8314V54.7771C78.3965 57.9182 75.8169 60.4646 72.6348 60.4646H41.321C38.0005 60.4646 35.3087 57.8075 35.3087 54.5298V49.8314C35.3087 49.0119 34.6358 48.3477 33.8057 48.3477H30.1106H26.2903C25.4602 48.3477 24.7836 49.0134 24.8219 49.832C25.6083 66.645 39.7042 79.2582 56.9779 79.2582Z" fill="#BEC8FF"/>
<path d="M53.8961 12.128V28.1618C53.8961 29.0051 53.2227 29.6932 52.3921 29.6932H40.1097C36.7872 29.6932 34.0938 32.4279 34.0938 35.8014V40.637C34.0938 41.4804 33.4205 42.1641 32.5899 42.1641H28.8299H25.07C24.2394 42.1641 23.5629 41.4785 23.5948 40.6357C24.2463 23.4509 35.8889 11.3309 52.3912 10.6366C53.2211 10.6016 53.8961 11.2846 53.8961 12.128Z" fill="#FFA70B"/>
<path d="M59.4555 12.128V28.1618C59.4555 29.0051 60.1233 29.6932 60.9471 29.6932H73.1303C76.3761 29.6932 79.0266 32.352 79.0956 35.6741V40.637C79.0956 41.4804 79.7635 42.1641 80.5873 42.1641H84.4407H88.2942C89.118 42.1641 89.789 41.4785 89.7567 40.6357C89.0979 23.4507 77.3285 11.3306 60.948 10.6365C60.1249 10.6017 59.4555 11.2846 59.4555 12.128Z" fill="#5C7EF1"/>
<path d="M39.0547 37.1238C39.0547 35.4655 40.3929 34.1211 42.0437 34.1211H71.9337C73.5845 34.1211 74.9228 35.4655 74.9228 37.1238V52.1375C74.9228 53.7959 73.5845 55.1403 71.9337 55.1403H42.0437C40.3929 55.1403 39.0547 53.7959 39.0547 52.1375V37.1238Z" fill="#02004E"/>
<path d="M48.333 49.5859C46.9669 49.5859 45.8594 48.4957 45.8594 47.1508V41.5115C45.8594 40.1666 46.9669 39.0763 48.333 39.0763V39.0763C49.6992 39.0763 50.8067 40.1666 50.8067 41.5115V47.1508C50.8067 48.4957 49.6992 49.5859 48.333 49.5859V49.5859Z" fill="#FFA70B"/>
<path d="M51.4297 64.3583C51.4297 62.9952 52.4818 63.1892 53.7797 63.1892H59.2217C60.5196 63.1892 61.3243 62.9952 61.3243 64.3583C61.3243 65.7214 59.2217 68.1254 56.377 68.1254C53.7797 68.1254 51.4297 65.7214 51.4297 64.3583Z" fill="#02004E"/>
<path d="M65.0362 49.5859C63.67 49.5859 62.5625 48.4957 62.5625 47.1508V41.5115C62.5625 40.1666 63.67 39.0763 65.0362 39.0763V39.0763C66.4023 39.0763 67.5098 40.1666 67.5098 41.5115V47.1508C67.5098 48.4957 66.4023 49.5859 65.0362 49.5859V49.5859Z" fill="#FFA70B"/>
</g>
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="filter0_d_311_30216" x="0" y="0.636719" width="134" height="108.621" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="10" dy="10"/>
<feGaussianBlur stdDeviation="10"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.00784314 0 0 0 0 0 0 0 0 0 0.305882 0 0 0 0.14 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_311_30216"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_311_30216" result="shape"/>
</filter>
<linearGradient id="bg" x1="8" y1="6" x2="56" y2="58" gradientUnits="userSpaceOnUse">
<stop stop-color="#050816" />
<stop offset="0.48" stop-color="#1E1B4B" />
<stop offset="1" stop-color="#312E81" />
</linearGradient>
<linearGradient id="accent" x1="18" y1="14" x2="48" y2="46" gradientUnits="userSpaceOnUse">
<stop stop-color="#38BDF8" />
<stop offset="0.52" stop-color="#A855F7" />
<stop offset="1" stop-color="#F472B6" />
</linearGradient>
</defs>
<rect x="4" y="4" width="56" height="56" rx="18" fill="url(#bg)" />
<circle cx="18" cy="18" r="12" fill="#38BDF8" fill-opacity="0.18" />
<circle cx="48" cy="46" r="14" fill="#A855F7" fill-opacity="0.24" />
<path
d="M18 46V18L31 36V18"
stroke="white"
stroke-width="4.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M36 18L46 46L52 32"
stroke="url(#accent)"
stroke-width="4.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,11 +1,10 @@
import React from 'react'
import { mdiLogout, mdiClose } from '@mdi/js'
import { mdiClose } from '@mdi/js'
import BaseIcon from './BaseIcon'
import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces'
import { creatorBrandName, creatorWebsite, platformName } from '../config'
import { useAppSelector } from '../stores/hooks'
import Link from 'next/link';
type Props = {
menu: MenuAsideItem[]
@ -25,23 +24,24 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
props.onAsideLgCloseClick()
}
return (
<aside
id='asideMenu'
className={`${className} zzz lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
>
<div
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
>
<div
className={`flex flex-row h-14 items-center justify-between ${asideBrandStyle}`}
>
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
<b className="font-black">AI Roleplay Bot Platform</b>
<b className="font-black">{platformName}</b>
<div className="mt-1 text-[10px] uppercase tracking-[0.24em] text-slate-400 dark:text-slate-500">
<a href={creatorWebsite} rel="noreferrer" target="_blank">
{creatorBrandName}
</a>
</div>
</div>
<button
className="hidden lg:inline-block xl:hidden p-3"

View File

@ -78,7 +78,7 @@ const CardBot_tags = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Bot</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Бот</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.botsOneListFormatter(item.bot) }
@ -90,7 +90,7 @@ const CardBot_tags = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Tag</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Тег</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.tagsOneListFormatter(item.tag) }
@ -105,7 +105,7 @@ const CardBot_tags = ({
))}
{!loading && bot_tags.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</ul>

View File

@ -48,7 +48,7 @@ const ListBot_tags = ({ bot_tags, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Bot</p>
<p className={'text-xs text-gray-500 '}>Бот</p>
<p className={'line-clamp-2'}>{ dataFormatter.botsOneListFormatter(item.bot) }</p>
</div>
@ -56,7 +56,7 @@ const ListBot_tags = ({ bot_tags, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Tag</p>
<p className={'text-xs text-gray-500 '}>Тег</p>
<p className={'line-clamp-2'}>{ dataFormatter.tagsOneListFormatter(item.tag) }</p>
</div>
@ -78,7 +78,7 @@ const ListBot_tags = ({ bot_tags, loading, onDelete, currentPage, numPages, onPa
))}
{!loading && bot_tags.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</div>

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureBot_tagsCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import { dataGridRuLocaleText } from '../../helpers/dataGridRuLocale';
@ -210,6 +211,7 @@ const TableSampleBot_tags = ({ filterItems, setFilterItems, filters, showGrid })
const dataGrid = (
<div className='relative overflow-x-auto'>
<DataGrid
localeText={dataGridRuLocaleText}
autoHeight
rowHeight={64}
sx={dataGridStyles}
@ -277,7 +279,7 @@ const TableSampleBot_tags = ({ filterItems, setFilterItems, filters, showGrid })
return (
<div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div>
<div className=" text-gray-500 font-bold">Фильтр</div>
<Field
className={controlClasses}
name='selectedField'
@ -301,7 +303,7 @@ const TableSampleBot_tags = ({ filterItems, setFilterItems, filters, showGrid })
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
Значение
</div>
<Field
className={controlClasses}
@ -311,7 +313,7 @@ const TableSampleBot_tags = ({ filterItems, setFilterItems, filters, showGrid })
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">Выберите значение</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -326,22 +328,22 @@ const TableSampleBot_tags = ({ filterItems, setFilterItems, filters, showGrid })
)?.number ? (
<div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div>
<div className=" text-gray-500 font-bold">От</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div>
<div className=" text-gray-500 font-bold">До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -356,12 +358,12 @@ const TableSampleBot_tags = ({ filterItems, setFilterItems, filters, showGrid })
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
От
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +371,11 @@ const TableSampleBot_tags = ({ filterItems, setFilterItems, filters, showGrid })
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<div className=' text-gray-500 font-bold'>До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +385,11 @@ const TableSampleBot_tags = ({ filterItems, setFilterItems, filters, showGrid })
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<div className=" text-gray-500 font-bold">Содержит</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder='Введите значение'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -395,12 +397,12 @@ const TableSampleBot_tags = ({ filterItems, setFilterItems, filters, showGrid })
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">Действие</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='Удалить'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +415,13 @@ const TableSampleBot_tags = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton
className="my-2 mr-3"
type='submit' color='info'
label='Apply'
label='Применить'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
type='reset' color='info' outline
label='Cancel'
label='Отмена'
onClick={handleReset}
/>
</div>
@ -429,9 +431,9 @@ const TableSampleBot_tags = ({ filterItems, setFilterItems, filters, showGrid })
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="Подтвердите действие"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
@ -450,7 +452,7 @@ const TableSampleBot_tags = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`Удалить ${selectedRows.length === 1 ? 'строку' : 'строки'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -43,7 +43,7 @@ export const loadColumns = async (
{
field: 'bot',
headerName: 'Bot',
headerName: 'Бот',
flex: 1,
minWidth: 120,
filterable: false,
@ -65,7 +65,7 @@ export const loadColumns = async (
{
field: 'tag',
headerName: 'Tag',
headerName: 'Тег',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -62,7 +62,7 @@ const CardBots = ({
className={'cursor-pointer'}
>
<ImageField
name={'Avatar'}
name={'Аватар'}
image={item.avatar}
className='w-12 h-12 md:w-full md:h-44 rounded-lg md:rounded-b-none overflow-hidden ring-1 ring-gray-900/10'
imageClassName='h-full w-full flex-none rounded-lg md:rounded-b-none bg-white object-cover'
@ -87,7 +87,7 @@ const CardBots = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Author</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Автор</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.usersOneListFormatter(item.author) }
@ -99,7 +99,7 @@ const CardBots = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>BotName</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Имя бота</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.name }
@ -111,11 +111,11 @@ const CardBots = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Avatar</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Аватар</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium'>
<ImageField
name={'Avatar'}
name={'Аватар'}
image={item.avatar}
className='mx-auto w-8 h-8'
/>
@ -127,7 +127,7 @@ const CardBots = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Backstory</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Предыстория</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.backstory }
@ -139,7 +139,7 @@ const CardBots = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Greeting</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Приветствие</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.greeting }
@ -151,7 +151,7 @@ const CardBots = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Description/Prompt</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Описание / промпт</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.description }
@ -163,7 +163,7 @@ const CardBots = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Visibility</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Видимость</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.visibility }
@ -187,7 +187,7 @@ const CardBots = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Draft</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Черновик</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.booleanFormatter(item.is_draft) }
@ -202,7 +202,7 @@ const CardBots = ({
))}
{!loading && bots.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</ul>

View File

@ -40,7 +40,7 @@ const ListBots = ({ bots, loading, onDelete, currentPage, numPages, onPageChange
<div className={`flex ${bgColor} ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} dark:bg-dark-900 border border-gray-600 items-center overflow-hidden`}>
<ImageField
name={'Avatar'}
name={'Аватар'}
image={item.avatar}
className='w-24 h-24 rounded-l overflow-hidden hidden md:block'
imageClassName={'rounded-l rounded-r-none h-full object-cover'}
@ -55,7 +55,7 @@ const ListBots = ({ bots, loading, onDelete, currentPage, numPages, onPageChange
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Author</p>
<p className={'text-xs text-gray-500 '}>Автор</p>
<p className={'line-clamp-2'}>{ dataFormatter.usersOneListFormatter(item.author) }</p>
</div>
@ -63,7 +63,7 @@ const ListBots = ({ bots, loading, onDelete, currentPage, numPages, onPageChange
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>BotName</p>
<p className={'text-xs text-gray-500 '}>Имя бота</p>
<p className={'line-clamp-2'}>{ item.name }</p>
</div>
@ -71,9 +71,9 @@ const ListBots = ({ bots, loading, onDelete, currentPage, numPages, onPageChange
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Avatar</p>
<p className={'text-xs text-gray-500 '}>Аватар</p>
<ImageField
name={'Avatar'}
name={'Аватар'}
image={item.avatar}
className='mx-auto w-8 h-8'
/>
@ -83,7 +83,7 @@ const ListBots = ({ bots, loading, onDelete, currentPage, numPages, onPageChange
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Backstory</p>
<p className={'text-xs text-gray-500 '}>Предыстория</p>
<p className={'line-clamp-2'}>{ item.backstory }</p>
</div>
@ -91,7 +91,7 @@ const ListBots = ({ bots, loading, onDelete, currentPage, numPages, onPageChange
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Greeting</p>
<p className={'text-xs text-gray-500 '}>Приветствие</p>
<p className={'line-clamp-2'}>{ item.greeting }</p>
</div>
@ -99,7 +99,7 @@ const ListBots = ({ bots, loading, onDelete, currentPage, numPages, onPageChange
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Description/Prompt</p>
<p className={'text-xs text-gray-500 '}>Описание / промпт</p>
<p className={'line-clamp-2'}>{ item.description }</p>
</div>
@ -107,7 +107,7 @@ const ListBots = ({ bots, loading, onDelete, currentPage, numPages, onPageChange
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Visibility</p>
<p className={'text-xs text-gray-500 '}>Видимость</p>
<p className={'line-clamp-2'}>{ item.visibility }</p>
</div>
@ -123,7 +123,7 @@ const ListBots = ({ bots, loading, onDelete, currentPage, numPages, onPageChange
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Draft</p>
<p className={'text-xs text-gray-500 '}>Черновик</p>
<p className={'line-clamp-2'}>{ dataFormatter.booleanFormatter(item.is_draft) }</p>
</div>
@ -145,7 +145,7 @@ const ListBots = ({ bots, loading, onDelete, currentPage, numPages, onPageChange
))}
{!loading && bots.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</div>

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureBotsCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import { dataGridRuLocaleText } from '../../helpers/dataGridRuLocale';
@ -210,6 +211,7 @@ const TableSampleBots = ({ filterItems, setFilterItems, filters, showGrid }) =>
const dataGrid = (
<div className='relative overflow-x-auto'>
<DataGrid
localeText={dataGridRuLocaleText}
autoHeight
rowHeight={64}
sx={dataGridStyles}
@ -277,7 +279,7 @@ const TableSampleBots = ({ filterItems, setFilterItems, filters, showGrid }) =>
return (
<div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div>
<div className=" text-gray-500 font-bold">Фильтр</div>
<Field
className={controlClasses}
name='selectedField'
@ -301,7 +303,7 @@ const TableSampleBots = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
Значение
</div>
<Field
className={controlClasses}
@ -311,7 +313,7 @@ const TableSampleBots = ({ filterItems, setFilterItems, filters, showGrid }) =>
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">Выберите значение</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -326,22 +328,22 @@ const TableSampleBots = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.number ? (
<div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div>
<div className=" text-gray-500 font-bold">От</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div>
<div className=" text-gray-500 font-bold">До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -356,12 +358,12 @@ const TableSampleBots = ({ filterItems, setFilterItems, filters, showGrid }) =>
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
От
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +371,11 @@ const TableSampleBots = ({ filterItems, setFilterItems, filters, showGrid }) =>
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<div className=' text-gray-500 font-bold'>До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +385,11 @@ const TableSampleBots = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<div className=" text-gray-500 font-bold">Содержит</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder='Введите значение'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -395,12 +397,12 @@ const TableSampleBots = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">Действие</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='Удалить'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +415,13 @@ const TableSampleBots = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton
className="my-2 mr-3"
type='submit' color='info'
label='Apply'
label='Применить'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
type='reset' color='info' outline
label='Cancel'
label='Отмена'
onClick={handleReset}
/>
</div>
@ -429,9 +431,9 @@ const TableSampleBots = ({ filterItems, setFilterItems, filters, showGrid }) =>
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="Подтвердите действие"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
@ -450,7 +452,7 @@ const TableSampleBots = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`Удалить ${selectedRows.length === 1 ? 'строку' : 'строки'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -43,7 +43,7 @@ export const loadColumns = async (
{
field: 'author',
headerName: 'Author',
headerName: 'Автор',
flex: 1,
minWidth: 120,
filterable: false,
@ -65,7 +65,7 @@ export const loadColumns = async (
{
field: 'name',
headerName: 'BotName',
headerName: 'Имя бота',
flex: 1,
minWidth: 120,
filterable: false,
@ -80,7 +80,7 @@ export const loadColumns = async (
{
field: 'avatar',
headerName: 'Avatar',
headerName: 'Аватар',
flex: 1,
minWidth: 120,
filterable: false,
@ -91,7 +91,7 @@ export const loadColumns = async (
sortable: false,
renderCell: (params: GridValueGetterParams) => (
<ImageField
name={'Avatar'}
name={'Аватар'}
image={params?.row?.avatar}
className='w-24 h-24 mx-auto lg:w-6 lg:h-6'
/>
@ -101,7 +101,7 @@ export const loadColumns = async (
{
field: 'backstory',
headerName: 'Backstory',
headerName: 'Предыстория',
flex: 1,
minWidth: 120,
filterable: false,
@ -116,7 +116,7 @@ export const loadColumns = async (
{
field: 'greeting',
headerName: 'Greeting',
headerName: 'Приветствие',
flex: 1,
minWidth: 120,
filterable: false,
@ -131,7 +131,7 @@ export const loadColumns = async (
{
field: 'description',
headerName: 'Description/Prompt',
headerName: 'Описание / промпт',
flex: 1,
minWidth: 120,
filterable: false,
@ -146,7 +146,7 @@ export const loadColumns = async (
{
field: 'visibility',
headerName: 'Visibility',
headerName: 'Видимость',
flex: 1,
minWidth: 120,
filterable: false,
@ -177,7 +177,7 @@ export const loadColumns = async (
{
field: 'is_draft',
headerName: 'Draft',
headerName: 'Черновик',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -33,7 +33,7 @@ const CardBoxModal = ({
const footer = (
<BaseButtons>
<BaseButton label={buttonLabel} color={buttonColor} onClick={onConfirm} />
{!!onCancel && <BaseButton label="Cancel" color={buttonColor} outline onClick={onCancel} />}
{!!onCancel && <BaseButton label="Отмена" color={buttonColor} outline onClick={onCancel} />}
</BaseButtons>
)

View File

@ -78,7 +78,7 @@ const CardConversations = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>User</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Пользователь</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.usersOneListFormatter(item.user) }
@ -90,7 +90,7 @@ const CardConversations = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Bot</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Бот</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.botsOneListFormatter(item.bot) }
@ -102,7 +102,7 @@ const CardConversations = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Persona</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Личность</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.personasOneListFormatter(item.persona) }
@ -114,7 +114,7 @@ const CardConversations = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>ConversationTitle</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Название диалога</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.title }
@ -126,7 +126,7 @@ const CardConversations = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Status</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Статус</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.status }
@ -138,7 +138,7 @@ const CardConversations = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>StartedAt</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Начало</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.dateTimeFormatter(item.started_at) }
@ -150,7 +150,7 @@ const CardConversations = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>LastMessageAt</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Последнее сообщение</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.dateTimeFormatter(item.last_message_at) }
@ -177,7 +177,7 @@ const CardConversations = ({
))}
{!loading && conversations.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</ul>

View File

@ -48,7 +48,7 @@ const ListConversations = ({ conversations, loading, onDelete, currentPage, numP
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>User</p>
<p className={'text-xs text-gray-500 '}>Пользователь</p>
<p className={'line-clamp-2'}>{ dataFormatter.usersOneListFormatter(item.user) }</p>
</div>
@ -56,7 +56,7 @@ const ListConversations = ({ conversations, loading, onDelete, currentPage, numP
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Bot</p>
<p className={'text-xs text-gray-500 '}>Бот</p>
<p className={'line-clamp-2'}>{ dataFormatter.botsOneListFormatter(item.bot) }</p>
</div>
@ -64,7 +64,7 @@ const ListConversations = ({ conversations, loading, onDelete, currentPage, numP
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Persona</p>
<p className={'text-xs text-gray-500 '}>Личность</p>
<p className={'line-clamp-2'}>{ dataFormatter.personasOneListFormatter(item.persona) }</p>
</div>
@ -72,7 +72,7 @@ const ListConversations = ({ conversations, loading, onDelete, currentPage, numP
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>ConversationTitle</p>
<p className={'text-xs text-gray-500 '}>Название диалога</p>
<p className={'line-clamp-2'}>{ item.title }</p>
</div>
@ -80,7 +80,7 @@ const ListConversations = ({ conversations, loading, onDelete, currentPage, numP
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Status</p>
<p className={'text-xs text-gray-500 '}>Статус</p>
<p className={'line-clamp-2'}>{ item.status }</p>
</div>
@ -88,7 +88,7 @@ const ListConversations = ({ conversations, loading, onDelete, currentPage, numP
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>StartedAt</p>
<p className={'text-xs text-gray-500 '}>Начало</p>
<p className={'line-clamp-2'}>{ dataFormatter.dateTimeFormatter(item.started_at) }</p>
</div>
@ -96,7 +96,7 @@ const ListConversations = ({ conversations, loading, onDelete, currentPage, numP
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>LastMessageAt</p>
<p className={'text-xs text-gray-500 '}>Последнее сообщение</p>
<p className={'line-clamp-2'}>{ dataFormatter.dateTimeFormatter(item.last_message_at) }</p>
</div>
@ -126,7 +126,7 @@ const ListConversations = ({ conversations, loading, onDelete, currentPage, numP
))}
{!loading && conversations.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</div>

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureConversationsCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import { dataGridRuLocaleText } from '../../helpers/dataGridRuLocale';
import BigCalendar from "../BigCalendar";
@ -219,6 +220,7 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
const dataGrid = (
<div className='relative overflow-x-auto'>
<DataGrid
localeText={dataGridRuLocaleText}
autoHeight
rowHeight={64}
sx={dataGridStyles}
@ -286,7 +288,7 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
return (
<div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div>
<div className=" text-gray-500 font-bold">Фильтр</div>
<Field
className={controlClasses}
name='selectedField'
@ -310,7 +312,7 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
Значение
</div>
<Field
className={controlClasses}
@ -320,7 +322,7 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">Выберите значение</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -335,22 +337,22 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
)?.number ? (
<div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div>
<div className=" text-gray-500 font-bold">От</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div>
<div className=" text-gray-500 font-bold">До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -365,12 +367,12 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
От
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -378,11 +380,11 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<div className=' text-gray-500 font-bold'>До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -392,11 +394,11 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<div className=" text-gray-500 font-bold">Содержит</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder='Введите значение'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -404,12 +406,12 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">Действие</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='Удалить'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -422,13 +424,13 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
<BaseButton
className="my-2 mr-3"
type='submit' color='info'
label='Apply'
label='Применить'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
type='reset' color='info' outline
label='Cancel'
label='Отмена'
onClick={handleReset}
/>
</div>
@ -438,9 +440,9 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="Подтвердите действие"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
@ -476,7 +478,7 @@ const TableSampleConversations = ({ filterItems, setFilterItems, filters, showGr
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`Удалить ${selectedRows.length === 1 ? 'строку' : 'строки'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -43,7 +43,7 @@ export const loadColumns = async (
{
field: 'user',
headerName: 'User',
headerName: 'Пользователь',
flex: 1,
minWidth: 120,
filterable: false,
@ -65,7 +65,7 @@ export const loadColumns = async (
{
field: 'bot',
headerName: 'Bot',
headerName: 'Бот',
flex: 1,
minWidth: 120,
filterable: false,
@ -87,7 +87,7 @@ export const loadColumns = async (
{
field: 'persona',
headerName: 'Persona',
headerName: 'Личность',
flex: 1,
minWidth: 120,
filterable: false,
@ -109,7 +109,7 @@ export const loadColumns = async (
{
field: 'title',
headerName: 'ConversationTitle',
headerName: 'Название диалога',
flex: 1,
minWidth: 120,
filterable: false,
@ -124,7 +124,7 @@ export const loadColumns = async (
{
field: 'status',
headerName: 'Status',
headerName: 'Статус',
flex: 1,
minWidth: 120,
filterable: false,
@ -139,7 +139,7 @@ export const loadColumns = async (
{
field: 'started_at',
headerName: 'StartedAt',
headerName: 'Начало',
flex: 1,
minWidth: 120,
filterable: false,
@ -157,7 +157,7 @@ export const loadColumns = async (
{
field: 'last_message_at',
headerName: 'LastMessageAt',
headerName: 'Последнее сообщение',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -26,7 +26,7 @@ const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => {
setFile(newFile);
setErrorMessage('');
} else {
setErrorMessage(`Allowed formats: ${formats}`);
setErrorMessage(`Разрешённые форматы: ${formats}`);
}
}
}
@ -97,8 +97,7 @@ const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => {
) : (
<>
<p className='mb-2 text-sm text-gray-500 dark:text-gray-400'>
<span className='font-semibold'>Click to upload</span> or drag
and drop
<span className='font-semibold'>Нажмите для загрузки</span> или перетащите файл сюда
</p>
{formats && (
<p className='text-xs text-gray-500 dark:text-gray-400'>

View File

@ -54,7 +54,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
});
const data = await response.json();
console.log('Error logs cleared:', data);
console.log('Логи ошибок очищены:', data);
}
}
@ -129,9 +129,9 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
});
const data = await response.json();
console.log('Error logs cleared:', data);
console.log('Логи ошибок очищены:', data);
} catch (e) {
console.error('Failed to clear error logs:', e);
console.error('Не удалось очистить логи ошибок:', e);
}
}
@ -143,9 +143,9 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
if (this.state.hasError) {
// Extract error details
const { error, errorInfo, showStack } = this.state;
const errorMessage = error?.message || 'An unexpected error occurred';
const errorMessage = error?.message || 'Произошла непредвиденная ошибка';
const stackTrace =
errorInfo?.componentStack || error?.stack || 'No stack trace available';
errorInfo?.componentStack || error?.stack || 'Стек вызовов недоступен';
return (
<div className='flex items-center justify-center min-h-screen bg-pavitra-300'>
@ -161,10 +161,10 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
<div className='space-y-2'>
<h2 className='text-xl font-semibold text-pavitra-900'>
Something went wrong
Что-то пошло не так
</h2>
<p className='text-pavitra-800'>
We&apos;re sorry, but we encountered an unexpected error.
Извините, приложение столкнулось с непредвиденной ошибкой.
</p>
</div>
@ -178,7 +178,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
onClick={this.toggleStack}
className='text-xs text-pavitra-800 flex items-center gap-1'
>
<span>{showStack ? 'Hide' : 'Show'} stack trace</span>
<span>{showStack ? 'Скрыть' : 'Показать'} стек вызовов</span>
<span className='text-xs'>{showStack ? '▲' : '▼'}</span>
</button>
@ -195,14 +195,14 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
className='w-full py-2 px-4 bg-pavitra-blue hover:bg-pavitra-900 text-white rounded-md transition-colors'
onClick={this.tryAgain}
>
Try Again
Попробовать снова
</button>
<button
className='w-full py-2 px-4 border border-pavitra-600 text-pavitra-800 hover:bg-pavitra-400 rounded-md transition-colors'
onClick={this.resetError}
>
Go Back
Вернуться назад
</button>
</div>
</div>

View File

@ -1,5 +1,5 @@
import React, { ReactNode } from 'react'
import { containerMaxW } from '../config'
import { containerMaxW, creatorBrandName, creatorCoCreatorName, creatorIdeaAuthor, creatorWebsite } from '../config'
import Logo from './Logo'
type Props = {
@ -15,20 +15,28 @@ export default function FooterBar({ children }: Props) {
<div className="text-center md:text-left mb-6 md:mb-0">
<b>
&copy;{year},{` `}
<a href="https://flatlogic.com/" rel="noreferrer" target="_blank">
Flatlogic
<a href={creatorWebsite} rel="noreferrer" target="_blank">
{creatorBrandName}
</a>
.
</b>
{` `}
{children}
<span className="block text-sm text-slate-500 dark:text-slate-400 md:inline md:ml-2">
Полноценное веб-приложение для AI-ролевых историй.
</span>
{children && <span className="block text-sm md:inline md:ml-2">{children}</span>}
<div className="mt-2 text-xs text-slate-500 dark:text-slate-400">
Идеи {creatorIdeaAuthor}. Реализация <a href={creatorWebsite} rel="noreferrer" target="_blank">{creatorBrandName}</a> и {creatorCoCreatorName}.
</div>
<div className="mt-1 text-xs text-slate-500 dark:text-slate-400">
На базе <a href="https://flatlogic.com/" rel="noreferrer" target="_blank">Flatlogic</a>
</div>
</div>
<div className="flex item-center md:py-2 gap-4">
<a href="https://flatlogic.com/" rel="noreferrer" target="_blank">
<Logo className="w-auto h-8 md:h-6 mx-auto" />
</a>
</div>
<div className="flex items-center md:py-2 gap-4">
<a href={creatorWebsite} rel="noreferrer" target="_blank">
<Logo className="mx-auto" />
</a>
</div>
</div>
</footer>
)

View File

@ -42,7 +42,7 @@ const KanbanCard = ({
href={`/${entityName}/${entityName}-view/?id=${item.id}`}
className={'text-base font-semibold'}
>
{item[showFieldName] ?? 'No data'}
{item[showFieldName] ?? 'Нет данных'}
</Link>
</div>
<div className={'flex items-center justify-between'}>

View File

@ -188,14 +188,14 @@ const KanbanColumn = ({
</div>
))}
{!data?.length && (
<p className={'text-center py-8 bg-midnightBlueTheme-cardColor dark:bg-dark-800'}>No data</p>
<p className={'text-center py-8 bg-midnightBlueTheme-cardColor dark:bg-dark-800'}>Нет данных</p>
)}
</div>
</CardBox>
<CardBoxModal
title='Please confirm'
title='Подтвердите действие'
buttonColor='info'
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'Удаляем...' : 'Подтвердить'}
isActive={!!itemIdToDelete}
onConfirm={onDeleteConfirm}
onCancel={() => setItemIdToDelete('')}

View File

@ -81,7 +81,7 @@ const ListActionsPopover = ({
href={linkView}
sx={{ justifyContent: "start" }}
>
View
Просмотр
</Button>
{hasUpdatePermission && (
<Button
@ -90,7 +90,7 @@ const ListActionsPopover = ({
href={linkEdit}
sx={{ justifyContent: "start" }}
>
Edit
Редактировать
</Button>
)}
{hasUpdatePermission && (
@ -103,7 +103,7 @@ const ListActionsPopover = ({
}}
sx={{ justifyContent: "start" }}
>
Delete
Удалить
</Button>
)}
</div>

View File

@ -1,4 +1,5 @@
import React from 'react'
import { creatorBrandName } from '../../config'
type Props = {
className?: string
@ -6,10 +7,41 @@ type Props = {
export default function Logo({ className = '' }: Props) {
return (
<img
src={"https://flatlogic.com/logo.svg"}
className={className}
alt={'Flatlogic logo'}>
</img>
<span className={`inline-flex items-center gap-3 text-left ${className}`.trim()}>
<span className="relative flex h-10 w-10 items-center justify-center overflow-hidden rounded-2xl bg-[#050816] shadow-[0_0_28px_rgba(139,92,246,0.35)] ring-1 ring-white/10">
<span className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.5),transparent_52%),radial-gradient(circle_at_bottom_right,rgba(168,85,247,0.7),transparent_56%)]" />
<svg
viewBox="0 0 40 40"
className="relative h-6 w-6"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M10 29V11L20 24V11"
stroke="white"
strokeWidth="3.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M23 11L30 29L34 20"
stroke="#7DD3FC"
strokeWidth="3.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
<span className="leading-none">
<span className="block text-sm font-black tracking-[0.02em] text-slate-900 dark:text-white">
{creatorBrandName}
</span>
<span className="mt-1 block text-[10px] font-medium uppercase tracking-[0.32em] text-slate-500 dark:text-slate-400">
AI roleplay studio
</span>
</span>
</span>
)
}

View File

@ -78,7 +78,7 @@ const CardMessages = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Conversation</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Диалог</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.conversationsOneListFormatter(item.conversation) }
@ -90,7 +90,7 @@ const CardMessages = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Role</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Роль</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.role }
@ -102,7 +102,7 @@ const CardMessages = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Content</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Содержимое</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.content }
@ -114,7 +114,7 @@ const CardMessages = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>SentAt</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Отправлено</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.dateTimeFormatter(item.sent_at) }
@ -126,7 +126,7 @@ const CardMessages = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>TokenCount</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Токены</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.token_count }
@ -141,7 +141,7 @@ const CardMessages = ({
))}
{!loading && messages.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</ul>

View File

@ -48,7 +48,7 @@ const ListMessages = ({ messages, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Conversation</p>
<p className={'text-xs text-gray-500 '}>Диалог</p>
<p className={'line-clamp-2'}>{ dataFormatter.conversationsOneListFormatter(item.conversation) }</p>
</div>
@ -56,7 +56,7 @@ const ListMessages = ({ messages, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Role</p>
<p className={'text-xs text-gray-500 '}>Роль</p>
<p className={'line-clamp-2'}>{ item.role }</p>
</div>
@ -64,7 +64,7 @@ const ListMessages = ({ messages, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Content</p>
<p className={'text-xs text-gray-500 '}>Содержимое</p>
<p className={'line-clamp-2'}>{ item.content }</p>
</div>
@ -72,7 +72,7 @@ const ListMessages = ({ messages, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>SentAt</p>
<p className={'text-xs text-gray-500 '}>Отправлено</p>
<p className={'line-clamp-2'}>{ dataFormatter.dateTimeFormatter(item.sent_at) }</p>
</div>
@ -80,7 +80,7 @@ const ListMessages = ({ messages, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>TokenCount</p>
<p className={'text-xs text-gray-500 '}>Токены</p>
<p className={'line-clamp-2'}>{ item.token_count }</p>
</div>
@ -102,7 +102,7 @@ const ListMessages = ({ messages, loading, onDelete, currentPage, numPages, onPa
))}
{!loading && messages.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</div>

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureMessagesCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import { dataGridRuLocaleText } from '../../helpers/dataGridRuLocale';
@ -210,6 +211,7 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
const dataGrid = (
<div className='relative overflow-x-auto'>
<DataGrid
localeText={dataGridRuLocaleText}
autoHeight
rowHeight={64}
sx={dataGridStyles}
@ -277,7 +279,7 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
return (
<div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div>
<div className=" text-gray-500 font-bold">Фильтр</div>
<Field
className={controlClasses}
name='selectedField'
@ -301,7 +303,7 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
Значение
</div>
<Field
className={controlClasses}
@ -311,7 +313,7 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">Выберите значение</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -326,22 +328,22 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
)?.number ? (
<div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div>
<div className=" text-gray-500 font-bold">От</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div>
<div className=" text-gray-500 font-bold">До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -356,12 +358,12 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
От
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +371,11 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<div className=' text-gray-500 font-bold'>До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +385,11 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<div className=" text-gray-500 font-bold">Содержит</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder='Введите значение'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -395,12 +397,12 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">Действие</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='Удалить'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +415,13 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton
className="my-2 mr-3"
type='submit' color='info'
label='Apply'
label='Применить'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
type='reset' color='info' outline
label='Cancel'
label='Отмена'
onClick={handleReset}
/>
</div>
@ -429,9 +431,9 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="Подтвердите действие"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
@ -450,7 +452,7 @@ const TableSampleMessages = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`Удалить ${selectedRows.length === 1 ? 'строку' : 'строки'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -43,7 +43,7 @@ export const loadColumns = async (
{
field: 'conversation',
headerName: 'Conversation',
headerName: 'Диалог',
flex: 1,
minWidth: 120,
filterable: false,
@ -65,7 +65,7 @@ export const loadColumns = async (
{
field: 'role',
headerName: 'Role',
headerName: 'Роль',
flex: 1,
minWidth: 120,
filterable: false,
@ -80,7 +80,7 @@ export const loadColumns = async (
{
field: 'content',
headerName: 'Content',
headerName: 'Содержимое',
flex: 1,
minWidth: 120,
filterable: false,
@ -95,7 +95,7 @@ export const loadColumns = async (
{
field: 'sent_at',
headerName: 'SentAt',
headerName: 'Отправлено',
flex: 1,
minWidth: 120,
filterable: false,
@ -113,7 +113,7 @@ export const loadColumns = async (
{
field: 'token_count',
headerName: 'TokenCount',
headerName: 'Токены',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -1,6 +1,5 @@
import React, {useEffect, useRef} from 'react'
import React, { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'
@ -66,9 +65,9 @@ export default function NavBarItem({ item }: Props) {
const getItemId = (label) => {
switch (label) {
case 'Light/Dark':
case 'Тема':
return 'themeToggle';
case 'Log out':
case 'Выйти':
return 'logout';
default:
return undefined;

View File

@ -56,9 +56,9 @@ export default function PasswordSetOrReset() {
<SectionFullScreen bg='violet'>
<div className='w-full flex flex-col items-center justify-center'>
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
{isInvitation && <p className='text-xl mb-2'>Set Password</p>}
{!isInvitation && <p className='text-xl mb-2'>Reset Password</p>}
<p className='text-base mb-4'>Enter your new password</p>
{isInvitation && <p className='text-xl mb-2'>Установить пароль</p>}
{!isInvitation && <p className='text-xl mb-2'>Сброс пароля</p>}
<p className='text-base mb-4'>Введите новый пароль</p>
<Formik
initialValues={{
@ -74,7 +74,7 @@ export default function PasswordSetOrReset() {
<Field
type='password'
name='password'
placeholder='Password'
placeholder='Пароль'
/>
</FormField>
<FormField
@ -82,7 +82,7 @@ export default function PasswordSetOrReset() {
<Field
type='password'
name='confirm'
placeholder='Confirm Password'
placeholder='Повторите пароль'
/>
</FormField>
@ -93,10 +93,10 @@ export default function PasswordSetOrReset() {
disabled={loading}
label={
loading
? 'Loading...'
? 'Загрузка...'
: isInvitation
? 'Set Password'
: 'Reset Password'
? 'Установить пароль'
: 'Сброс пароля'
}
color='info'
/>

View File

@ -78,7 +78,7 @@ const CardPermissions = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Name</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Название</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.name }
@ -93,7 +93,7 @@ const CardPermissions = ({
))}
{!loading && permissions.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</ul>

View File

@ -48,7 +48,7 @@ const ListPermissions = ({ permissions, loading, onDelete, currentPage, numPages
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Name</p>
<p className={'text-xs text-gray-500 '}>Название</p>
<p className={'line-clamp-2'}>{ item.name }</p>
</div>
@ -70,7 +70,7 @@ const ListPermissions = ({ permissions, loading, onDelete, currentPage, numPages
))}
{!loading && permissions.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</div>

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configurePermissionsCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import { dataGridRuLocaleText } from '../../helpers/dataGridRuLocale';
@ -210,6 +211,7 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
const dataGrid = (
<div className='relative overflow-x-auto'>
<DataGrid
localeText={dataGridRuLocaleText}
autoHeight
rowHeight={64}
sx={dataGridStyles}
@ -277,7 +279,7 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
return (
<div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div>
<div className=" text-gray-500 font-bold">Фильтр</div>
<Field
className={controlClasses}
name='selectedField'
@ -301,7 +303,7 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
Значение
</div>
<Field
className={controlClasses}
@ -311,7 +313,7 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">Выберите значение</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -326,22 +328,22 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
)?.number ? (
<div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div>
<div className=" text-gray-500 font-bold">От</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div>
<div className=" text-gray-500 font-bold">До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -356,12 +358,12 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
От
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +371,11 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<div className=' text-gray-500 font-bold'>До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +385,11 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<div className=" text-gray-500 font-bold">Содержит</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder='Введите значение'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -395,12 +397,12 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">Действие</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='Удалить'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +415,13 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton
className="my-2 mr-3"
type='submit' color='info'
label='Apply'
label='Применить'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
type='reset' color='info' outline
label='Cancel'
label='Отмена'
onClick={handleReset}
/>
</div>
@ -429,9 +431,9 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="Подтвердите действие"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
@ -450,7 +452,7 @@ const TableSamplePermissions = ({ filterItems, setFilterItems, filters, showGrid
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`Удалить ${selectedRows.length === 1 ? 'строку' : 'строки'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -43,7 +43,7 @@ export const loadColumns = async (
{
field: 'name',
headerName: 'Name',
headerName: 'Название',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -62,7 +62,7 @@ const CardPersonas = ({
className={'cursor-pointer'}
>
<ImageField
name={'Avatar'}
name={'Аватар'}
image={item.avatar}
className='w-12 h-12 md:w-full md:h-44 rounded-lg md:rounded-b-none overflow-hidden ring-1 ring-gray-900/10'
imageClassName='h-full w-full flex-none rounded-lg md:rounded-b-none bg-white object-cover'
@ -87,7 +87,7 @@ const CardPersonas = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>User</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Пользователь</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.usersOneListFormatter(item.user) }
@ -99,7 +99,7 @@ const CardPersonas = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>PersonaName</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Имя личности</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.name }
@ -111,11 +111,11 @@ const CardPersonas = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Avatar</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Аватар</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium'>
<ImageField
name={'Avatar'}
name={'Аватар'}
image={item.avatar}
className='mx-auto w-8 h-8'
/>
@ -127,7 +127,7 @@ const CardPersonas = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Description</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Описание</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.description }
@ -139,7 +139,7 @@ const CardPersonas = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Appearance</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Внешность</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.appearance }
@ -151,7 +151,7 @@ const CardPersonas = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>BirthDate</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Дата рождения</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.dateTimeFormatter(item.birth_date) }
@ -163,7 +163,7 @@ const CardPersonas = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Height/Weight</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Рост / вес</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.height_weight }
@ -175,7 +175,7 @@ const CardPersonas = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Age</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Возраст</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.age }
@ -187,7 +187,7 @@ const CardPersonas = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>ActivePersona</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Активная личность</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.booleanFormatter(item.is_active) }
@ -202,7 +202,7 @@ const CardPersonas = ({
))}
{!loading && personas.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</ul>

View File

@ -40,7 +40,7 @@ const ListPersonas = ({ personas, loading, onDelete, currentPage, numPages, onPa
<div className={`flex ${bgColor} ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} dark:bg-dark-900 border border-gray-600 items-center overflow-hidden`}>
<ImageField
name={'Avatar'}
name={'Аватар'}
image={item.avatar}
className='w-24 h-24 rounded-l overflow-hidden hidden md:block'
imageClassName={'rounded-l rounded-r-none h-full object-cover'}
@ -55,7 +55,7 @@ const ListPersonas = ({ personas, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>User</p>
<p className={'text-xs text-gray-500 '}>Пользователь</p>
<p className={'line-clamp-2'}>{ dataFormatter.usersOneListFormatter(item.user) }</p>
</div>
@ -63,7 +63,7 @@ const ListPersonas = ({ personas, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>PersonaName</p>
<p className={'text-xs text-gray-500 '}>Имя личности</p>
<p className={'line-clamp-2'}>{ item.name }</p>
</div>
@ -71,9 +71,9 @@ const ListPersonas = ({ personas, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Avatar</p>
<p className={'text-xs text-gray-500 '}>Аватар</p>
<ImageField
name={'Avatar'}
name={'Аватар'}
image={item.avatar}
className='mx-auto w-8 h-8'
/>
@ -83,7 +83,7 @@ const ListPersonas = ({ personas, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Description</p>
<p className={'text-xs text-gray-500 '}>Описание</p>
<p className={'line-clamp-2'}>{ item.description }</p>
</div>
@ -91,7 +91,7 @@ const ListPersonas = ({ personas, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Appearance</p>
<p className={'text-xs text-gray-500 '}>Внешность</p>
<p className={'line-clamp-2'}>{ item.appearance }</p>
</div>
@ -99,7 +99,7 @@ const ListPersonas = ({ personas, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>BirthDate</p>
<p className={'text-xs text-gray-500 '}>Дата рождения</p>
<p className={'line-clamp-2'}>{ dataFormatter.dateTimeFormatter(item.birth_date) }</p>
</div>
@ -107,7 +107,7 @@ const ListPersonas = ({ personas, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Height/Weight</p>
<p className={'text-xs text-gray-500 '}>Рост / вес</p>
<p className={'line-clamp-2'}>{ item.height_weight }</p>
</div>
@ -115,7 +115,7 @@ const ListPersonas = ({ personas, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Age</p>
<p className={'text-xs text-gray-500 '}>Возраст</p>
<p className={'line-clamp-2'}>{ item.age }</p>
</div>
@ -123,7 +123,7 @@ const ListPersonas = ({ personas, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>ActivePersona</p>
<p className={'text-xs text-gray-500 '}>Активная личность</p>
<p className={'line-clamp-2'}>{ dataFormatter.booleanFormatter(item.is_active) }</p>
</div>
@ -145,7 +145,7 @@ const ListPersonas = ({ personas, loading, onDelete, currentPage, numPages, onPa
))}
{!loading && personas.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</div>

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configurePersonasCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import { dataGridRuLocaleText } from '../../helpers/dataGridRuLocale';
import CardPersonas from './CardPersonas';
@ -212,6 +213,7 @@ const TableSamplePersonas = ({ filterItems, setFilterItems, filters, showGrid })
const dataGrid = (
<div className='relative overflow-x-auto'>
<DataGrid
localeText={dataGridRuLocaleText}
autoHeight
rowHeight={64}
sx={dataGridStyles}
@ -279,7 +281,7 @@ const TableSamplePersonas = ({ filterItems, setFilterItems, filters, showGrid })
return (
<div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div>
<div className=" text-gray-500 font-bold">Фильтр</div>
<Field
className={controlClasses}
name='selectedField'
@ -303,7 +305,7 @@ const TableSamplePersonas = ({ filterItems, setFilterItems, filters, showGrid })
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
Значение
</div>
<Field
className={controlClasses}
@ -313,7 +315,7 @@ const TableSamplePersonas = ({ filterItems, setFilterItems, filters, showGrid })
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">Выберите значение</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -328,22 +330,22 @@ const TableSamplePersonas = ({ filterItems, setFilterItems, filters, showGrid })
)?.number ? (
<div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div>
<div className=" text-gray-500 font-bold">От</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div>
<div className=" text-gray-500 font-bold">До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -358,12 +360,12 @@ const TableSamplePersonas = ({ filterItems, setFilterItems, filters, showGrid })
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
От
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -371,11 +373,11 @@ const TableSamplePersonas = ({ filterItems, setFilterItems, filters, showGrid })
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<div className=' text-gray-500 font-bold'>До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -385,11 +387,11 @@ const TableSamplePersonas = ({ filterItems, setFilterItems, filters, showGrid })
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<div className=" text-gray-500 font-bold">Содержит</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder='Введите значение'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -397,12 +399,12 @@ const TableSamplePersonas = ({ filterItems, setFilterItems, filters, showGrid })
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">Действие</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='Удалить'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -415,13 +417,13 @@ const TableSamplePersonas = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton
className="my-2 mr-3"
type='submit' color='info'
label='Apply'
label='Применить'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
type='reset' color='info' outline
label='Cancel'
label='Отмена'
onClick={handleReset}
/>
</div>
@ -431,9 +433,9 @@ const TableSamplePersonas = ({ filterItems, setFilterItems, filters, showGrid })
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="Подтвердите действие"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
@ -463,7 +465,7 @@ const TableSamplePersonas = ({ filterItems, setFilterItems, filters, showGrid })
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`Удалить ${selectedRows.length === 1 ? 'строку' : 'строки'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -43,7 +43,7 @@ export const loadColumns = async (
{
field: 'user',
headerName: 'User',
headerName: 'Пользователь',
flex: 1,
minWidth: 120,
filterable: false,
@ -65,7 +65,7 @@ export const loadColumns = async (
{
field: 'name',
headerName: 'PersonaName',
headerName: 'Имя личности',
flex: 1,
minWidth: 120,
filterable: false,
@ -80,7 +80,7 @@ export const loadColumns = async (
{
field: 'avatar',
headerName: 'Avatar',
headerName: 'Аватар',
flex: 1,
minWidth: 120,
filterable: false,
@ -91,7 +91,7 @@ export const loadColumns = async (
sortable: false,
renderCell: (params: GridValueGetterParams) => (
<ImageField
name={'Avatar'}
name={'Аватар'}
image={params?.row?.avatar}
className='w-24 h-24 mx-auto lg:w-6 lg:h-6'
/>
@ -101,7 +101,7 @@ export const loadColumns = async (
{
field: 'description',
headerName: 'Description',
headerName: 'Описание',
flex: 1,
minWidth: 120,
filterable: false,
@ -116,7 +116,7 @@ export const loadColumns = async (
{
field: 'appearance',
headerName: 'Appearance',
headerName: 'Внешность',
flex: 1,
minWidth: 120,
filterable: false,
@ -131,7 +131,7 @@ export const loadColumns = async (
{
field: 'birth_date',
headerName: 'BirthDate',
headerName: 'Дата рождения',
flex: 1,
minWidth: 120,
filterable: false,
@ -149,7 +149,7 @@ export const loadColumns = async (
{
field: 'height_weight',
headerName: 'Height/Weight',
headerName: 'Рост / вес',
flex: 1,
minWidth: 120,
filterable: false,
@ -164,7 +164,7 @@ export const loadColumns = async (
{
field: 'age',
headerName: 'Age',
headerName: 'Возраст',
flex: 1,
minWidth: 120,
filterable: false,
@ -180,7 +180,7 @@ export const loadColumns = async (
{
field: 'is_active',
headerName: 'ActivePersona',
headerName: 'Активная личность',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -78,7 +78,7 @@ const CardRoles = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Name</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Название</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.name }
@ -90,7 +90,7 @@ const CardRoles = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Permissions</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Права</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.permissionsManyListFormatter(item.permissions).join(', ')}
@ -105,7 +105,7 @@ const CardRoles = ({
))}
{!loading && roles.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</ul>

View File

@ -48,7 +48,7 @@ const ListRoles = ({ roles, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Name</p>
<p className={'text-xs text-gray-500 '}>Название</p>
<p className={'line-clamp-2'}>{ item.name }</p>
</div>
@ -56,7 +56,7 @@ const ListRoles = ({ roles, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Permissions</p>
<p className={'text-xs text-gray-500 '}>Права</p>
<p className={'line-clamp-2'}>{ dataFormatter.permissionsManyListFormatter(item.permissions).join(', ')}</p>
</div>
@ -78,7 +78,7 @@ const ListRoles = ({ roles, loading, onDelete, currentPage, numPages, onPageChan
))}
{!loading && roles.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</div>

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureRolesCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import { dataGridRuLocaleText } from '../../helpers/dataGridRuLocale';
@ -210,6 +211,7 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
const dataGrid = (
<div id="rolesTable" className='relative overflow-x-auto'>
<DataGrid
localeText={dataGridRuLocaleText}
autoHeight
rowHeight={64}
sx={dataGridStyles}
@ -277,7 +279,7 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
return (
<div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div>
<div className=" text-gray-500 font-bold">Фильтр</div>
<Field
className={controlClasses}
name='selectedField'
@ -301,7 +303,7 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
Значение
</div>
<Field
className={controlClasses}
@ -311,7 +313,7 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">Выберите значение</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -326,22 +328,22 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.number ? (
<div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div>
<div className=" text-gray-500 font-bold">От</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div>
<div className=" text-gray-500 font-bold">До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -356,12 +358,12 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
От
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +371,11 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<div className=' text-gray-500 font-bold'>До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +385,11 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<div className=" text-gray-500 font-bold">Содержит</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder='Введите значение'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -395,12 +397,12 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">Действие</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='Удалить'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +415,13 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton
className="my-2 mr-3"
type='submit' color='info'
label='Apply'
label='Применить'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
type='reset' color='info' outline
label='Cancel'
label='Отмена'
onClick={handleReset}
/>
</div>
@ -429,9 +431,9 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="Подтвердите действие"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
@ -450,7 +452,7 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`Удалить ${selectedRows.length === 1 ? 'строку' : 'строки'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -43,7 +43,7 @@ export const loadColumns = async (
{
field: 'name',
headerName: 'Name',
headerName: 'Название',
flex: 1,
minWidth: 120,
filterable: false,
@ -58,7 +58,7 @@ export const loadColumns = async (
{
field: 'permissions',
headerName: 'Permissions',
headerName: 'Права',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -7,39 +7,48 @@ const Search = () => {
const router = useRouter();
const focusRing = useAppSelector((state) => state.style.focusRingColor);
const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
const validateSearch = (value) => {
let error;
if (!value) {
error = 'Required';
} else if (value.length < 2) {
error = 'Minimum length: 2 characters';
if (!value?.trim()) {
return 'Введите запрос';
}
return error;
return undefined;
};
return (
<Formik
initialValues={{
search: '',
}}
onSubmit={(values, { setSubmitting, resetForm }) => {
router.push(`/search?query=${values.search}`);
const trimmedSearch = values.search.trim();
if (!trimmedSearch) {
setSubmitting(false);
return;
}
router.push({
pathname: '/search',
query: { query: trimmedSearch },
});
resetForm();
setSubmitting(false);
}}
validateOnBlur={false}
validateOnChange={false}
>
{({ errors, touched, values }) => (
<Form style={{width: '300px'}} >
{({ errors, touched }) => (
<Form style={{ width: '300px' }}>
<Field
id='search'
name='search'
validate={validateSearch}
placeholder='Search'
className={` ${corners} dark:bg-dark-900 bg-midnightBlueTheme-outsideCardColor dark:border-dark-700 p-2 relative ml-2 w-full dark:placeholder-dark-600 ${focusRing} shadow-none`}
placeholder='Свободный поиск'
className={` ${corners} dark:bg-dark-900 bg-midnightBlueTheme-outsideCardColor dark:border-dark-700 p-2 relative ml-2 w-full dark:placeholder-dark-600 ${focusRing} shadow-none`}
/>
{errors.search && touched.search && values.search.length < 2 ? (
{errors.search && touched.search ? (
<div className='text-red-500 text-sm ml-2 absolute'>{errors.search}</div>
) : null}
</Form>
@ -47,4 +56,5 @@ const Search = () => {
</Formik>
);
};
export default Search;

View File

@ -5,61 +5,50 @@ import { humanize } from '../helpers/humanize';
const SearchResults = ({ searchResults, searchQuery }) => {
const router = useRouter();
const tableNames = Object.keys(searchResults || {});
return (
<>
<p className={'block font-bold mb-2'}>Matches with: {searchQuery}</p>
{Object.keys(searchResults).map((tableName) => (
<>
<p className={'block font-bold mb-2'}>{humanize(tableName)}</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
key={tableName}
>
<p className={'mb-2 block font-bold'}>Совпадения по запросу: {searchQuery}</p>
{tableNames.map((tableName) => (
<React.Fragment key={tableName}>
<p className={'mb-2 block font-bold'}>{humanize(tableName)}</p>
<CardBox className='mb-6 overflow-hidden rounded border border-gray-300' hasTable>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
{searchResults[tableName].length > 0 &&
Object.keys(searchResults[tableName][0]).map((key) => {
if (
key !== 'tableName' &&
key !== 'id' &&
key !== 'matchAttribute'
) {
if (key !== 'tableName' && key !== 'id' && key !== 'matchAttribute') {
return (
<th data-label={key} key={key}>
{humanize(key)}
</th>
);
}
return null;
})}
</tr>
</thead>
<tbody>
{searchResults[tableName].map((item, index) => (
<tr key={index}>
<tr key={`${tableName}-${item.id || index}`}>
{Object.keys(item).map((key) => {
if (
key !== 'tableName' &&
key !== 'id' &&
key !== 'matchAttribute'
) {
if (key !== 'tableName' && key !== 'id' && key !== 'matchAttribute') {
return (
<td
key={key}
onClick={() =>
router.push(
`/${tableName}/${tableName}-view/?id=${item['id']}`,
)
router.push(`/${tableName}/${tableName}-view/?id=${item.id}`)
}
>
{item[key]}
</td>
);
}
return null;
})}
</tr>
@ -67,15 +56,11 @@ const SearchResults = ({ searchResults, searchQuery }) => {
</tbody>
</table>
</div>
{!Object.keys(searchResults).length && (
<div className={'text-center py-4'}>No data</div>
)}
{!tableNames.length && <div className={'py-4 text-center'}>Нет данных</div>}
</CardBox>
</>
</React.Fragment>
))}
{!Object.keys(searchResults).length && (
<div className={'py-4'}>No matches</div>
)}
{!tableNames.length && <div className={'py-4'}>Совпадений не найдено</div>}
</>
);
};

View File

@ -75,7 +75,7 @@ export const SmartWidget = ({ widget, userId, admin, roleId }) => {
)
) : (
<div className='text-center text-red-400'>
Something went wrong, please try again or use a different query.
Что-то пошло не так. Попробуйте ещё раз или измените запрос.
</div>
)}
</div>

View File

@ -49,9 +49,9 @@ const TableSampleClients = () => {
</CardBoxModal>
<CardBoxModal
title="Please confirm"
title="Подтвердите действие"
buttonColor="danger"
buttonLabel="Confirm"
buttonLabel="Подтвердить"
isActive={isModalTrashActive}
onConfirm={handleModalAction}
onCancel={handleModalAction}
@ -66,7 +66,7 @@ const TableSampleClients = () => {
<thead>
<tr>
<th />
<th>Name</th>
<th>Название</th>
<th>Company</th>
<th>City</th>
<th>Progress</th>
@ -80,7 +80,7 @@ const TableSampleClients = () => {
<td className="border-b-0 lg:w-6 before:hidden">
<UserAvatar username={client.name} className="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
</td>
<td data-label="Name">{client.name}</td>
<td data-label="Название">{client.name}</td>
<td data-label="Company">{client.company}</td>
<td data-label="City">{client.city}</td>
<td data-label="Progress" className="lg:w-32">

View File

@ -78,7 +78,7 @@ const CardTags = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>TagName</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Имя тега</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.name }
@ -90,7 +90,7 @@ const CardTags = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Slug</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Слаг</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.slug }
@ -105,7 +105,7 @@ const CardTags = ({
))}
{!loading && tags.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</ul>

View File

@ -48,7 +48,7 @@ const ListTags = ({ tags, loading, onDelete, currentPage, numPages, onPageChange
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>TagName</p>
<p className={'text-xs text-gray-500 '}>Имя тега</p>
<p className={'line-clamp-2'}>{ item.name }</p>
</div>
@ -56,7 +56,7 @@ const ListTags = ({ tags, loading, onDelete, currentPage, numPages, onPageChange
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Slug</p>
<p className={'text-xs text-gray-500 '}>Слаг</p>
<p className={'line-clamp-2'}>{ item.slug }</p>
</div>
@ -78,7 +78,7 @@ const ListTags = ({ tags, loading, onDelete, currentPage, numPages, onPageChange
))}
{!loading && tags.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</div>

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureTagsCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import { dataGridRuLocaleText } from '../../helpers/dataGridRuLocale';
import ListTags from './ListTags';
@ -212,6 +213,7 @@ const TableSampleTags = ({ filterItems, setFilterItems, filters, showGrid }) =>
const dataGrid = (
<div className='relative overflow-x-auto'>
<DataGrid
localeText={dataGridRuLocaleText}
autoHeight
rowHeight={64}
sx={dataGridStyles}
@ -279,7 +281,7 @@ const TableSampleTags = ({ filterItems, setFilterItems, filters, showGrid }) =>
return (
<div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div>
<div className=" text-gray-500 font-bold">Фильтр</div>
<Field
className={controlClasses}
name='selectedField'
@ -303,7 +305,7 @@ const TableSampleTags = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
Значение
</div>
<Field
className={controlClasses}
@ -313,7 +315,7 @@ const TableSampleTags = ({ filterItems, setFilterItems, filters, showGrid }) =>
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">Выберите значение</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -328,22 +330,22 @@ const TableSampleTags = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.number ? (
<div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div>
<div className=" text-gray-500 font-bold">От</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div>
<div className=" text-gray-500 font-bold">До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -358,12 +360,12 @@ const TableSampleTags = ({ filterItems, setFilterItems, filters, showGrid }) =>
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
От
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -371,11 +373,11 @@ const TableSampleTags = ({ filterItems, setFilterItems, filters, showGrid }) =>
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<div className=' text-gray-500 font-bold'>До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -385,11 +387,11 @@ const TableSampleTags = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<div className=" text-gray-500 font-bold">Содержит</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder='Введите значение'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -397,12 +399,12 @@ const TableSampleTags = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">Действие</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='Удалить'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -415,13 +417,13 @@ const TableSampleTags = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton
className="my-2 mr-3"
type='submit' color='info'
label='Apply'
label='Применить'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
type='reset' color='info' outline
label='Cancel'
label='Отмена'
onClick={handleReset}
/>
</div>
@ -431,9 +433,9 @@ const TableSampleTags = ({ filterItems, setFilterItems, filters, showGrid }) =>
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="Подтвердите действие"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
@ -463,7 +465,7 @@ const TableSampleTags = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`Удалить ${selectedRows.length === 1 ? 'строку' : 'строки'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -43,7 +43,7 @@ export const loadColumns = async (
{
field: 'name',
headerName: 'TagName',
headerName: 'Имя тега',
flex: 1,
minWidth: 120,
filterable: false,
@ -58,7 +58,7 @@ export const loadColumns = async (
{
field: 'slug',
headerName: 'Slug',
headerName: 'Слаг',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -78,7 +78,7 @@ const CardUser_search_logs = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>User</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Пользователь</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.usersOneListFormatter(item.user) }
@ -90,7 +90,7 @@ const CardUser_search_logs = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Query</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Запрос</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.query }
@ -102,7 +102,7 @@ const CardUser_search_logs = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Scope</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Область поиска</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.scope }
@ -114,7 +114,7 @@ const CardUser_search_logs = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>SearchedAt</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Дата поиска</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.dateTimeFormatter(item.searched_at) }
@ -129,7 +129,7 @@ const CardUser_search_logs = ({
))}
{!loading && user_search_logs.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</ul>

View File

@ -48,7 +48,7 @@ const ListUser_search_logs = ({ user_search_logs, loading, onDelete, currentPage
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>User</p>
<p className={'text-xs text-gray-500 '}>Пользователь</p>
<p className={'line-clamp-2'}>{ dataFormatter.usersOneListFormatter(item.user) }</p>
</div>
@ -56,7 +56,7 @@ const ListUser_search_logs = ({ user_search_logs, loading, onDelete, currentPage
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Query</p>
<p className={'text-xs text-gray-500 '}>Запрос</p>
<p className={'line-clamp-2'}>{ item.query }</p>
</div>
@ -64,7 +64,7 @@ const ListUser_search_logs = ({ user_search_logs, loading, onDelete, currentPage
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Scope</p>
<p className={'text-xs text-gray-500 '}>Область поиска</p>
<p className={'line-clamp-2'}>{ item.scope }</p>
</div>
@ -72,7 +72,7 @@ const ListUser_search_logs = ({ user_search_logs, loading, onDelete, currentPage
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>SearchedAt</p>
<p className={'text-xs text-gray-500 '}>Дата поиска</p>
<p className={'line-clamp-2'}>{ dataFormatter.dateTimeFormatter(item.searched_at) }</p>
</div>
@ -94,7 +94,7 @@ const ListUser_search_logs = ({ user_search_logs, loading, onDelete, currentPage
))}
{!loading && user_search_logs.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</div>

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureUser_search_logsCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import { dataGridRuLocaleText } from '../../helpers/dataGridRuLocale';
@ -210,6 +211,7 @@ const TableSampleUser_search_logs = ({ filterItems, setFilterItems, filters, sho
const dataGrid = (
<div className='relative overflow-x-auto'>
<DataGrid
localeText={dataGridRuLocaleText}
autoHeight
rowHeight={64}
sx={dataGridStyles}
@ -277,7 +279,7 @@ const TableSampleUser_search_logs = ({ filterItems, setFilterItems, filters, sho
return (
<div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div>
<div className=" text-gray-500 font-bold">Фильтр</div>
<Field
className={controlClasses}
name='selectedField'
@ -301,7 +303,7 @@ const TableSampleUser_search_logs = ({ filterItems, setFilterItems, filters, sho
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
Значение
</div>
<Field
className={controlClasses}
@ -311,7 +313,7 @@ const TableSampleUser_search_logs = ({ filterItems, setFilterItems, filters, sho
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">Выберите значение</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -326,22 +328,22 @@ const TableSampleUser_search_logs = ({ filterItems, setFilterItems, filters, sho
)?.number ? (
<div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div>
<div className=" text-gray-500 font-bold">От</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div>
<div className=" text-gray-500 font-bold">До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -356,12 +358,12 @@ const TableSampleUser_search_logs = ({ filterItems, setFilterItems, filters, sho
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
От
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +371,11 @@ const TableSampleUser_search_logs = ({ filterItems, setFilterItems, filters, sho
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<div className=' text-gray-500 font-bold'>До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +385,11 @@ const TableSampleUser_search_logs = ({ filterItems, setFilterItems, filters, sho
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<div className=" text-gray-500 font-bold">Содержит</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder='Введите значение'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -395,12 +397,12 @@ const TableSampleUser_search_logs = ({ filterItems, setFilterItems, filters, sho
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">Действие</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='Удалить'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +415,13 @@ const TableSampleUser_search_logs = ({ filterItems, setFilterItems, filters, sho
<BaseButton
className="my-2 mr-3"
type='submit' color='info'
label='Apply'
label='Применить'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
type='reset' color='info' outline
label='Cancel'
label='Отмена'
onClick={handleReset}
/>
</div>
@ -429,9 +431,9 @@ const TableSampleUser_search_logs = ({ filterItems, setFilterItems, filters, sho
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="Подтвердите действие"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
@ -450,7 +452,7 @@ const TableSampleUser_search_logs = ({ filterItems, setFilterItems, filters, sho
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`Удалить ${selectedRows.length === 1 ? 'строку' : 'строки'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -43,7 +43,7 @@ export const loadColumns = async (
{
field: 'user',
headerName: 'User',
headerName: 'Пользователь',
flex: 1,
minWidth: 120,
filterable: false,
@ -65,7 +65,7 @@ export const loadColumns = async (
{
field: 'query',
headerName: 'Query',
headerName: 'Запрос',
flex: 1,
minWidth: 120,
filterable: false,
@ -80,7 +80,7 @@ export const loadColumns = async (
{
field: 'scope',
headerName: 'Scope',
headerName: 'Область поиска',
flex: 1,
minWidth: 120,
filterable: false,
@ -95,7 +95,7 @@ export const loadColumns = async (
{
field: 'searched_at',
headerName: 'SearchedAt',
headerName: 'Дата поиска',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -62,7 +62,7 @@ const CardUsers = ({
className={'cursor-pointer'}
>
<ImageField
name={'Avatar'}
name={'Аватар'}
image={item.avatar}
className='w-12 h-12 md:w-full md:h-44 rounded-lg md:rounded-b-none overflow-hidden ring-1 ring-gray-900/10'
imageClassName='h-full w-full flex-none rounded-lg md:rounded-b-none bg-white object-cover'
@ -87,7 +87,7 @@ const CardUsers = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>First Name</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Имя</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.firstName }
@ -99,7 +99,7 @@ const CardUsers = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Last Name</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Фамилия</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.lastName }
@ -111,7 +111,7 @@ const CardUsers = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Phone Number</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Телефон</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.phoneNumber }
@ -123,7 +123,7 @@ const CardUsers = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>E-Mail</dt>
<dt className=' text-gray-500 dark:text-dark-600'>E-mail</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.email }
@ -135,7 +135,7 @@ const CardUsers = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Disabled</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Отключён</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.booleanFormatter(item.disabled) }
@ -147,11 +147,11 @@ const CardUsers = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Avatar</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Аватар</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium'>
<ImageField
name={'Avatar'}
name={'Аватар'}
image={item.avatar}
className='mx-auto w-8 h-8'
/>
@ -163,7 +163,7 @@ const CardUsers = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>App Role</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Роль приложения</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.rolesOneListFormatter(item.app_role) }
@ -175,7 +175,7 @@ const CardUsers = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Custom Permissions</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Индивидуальные права</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.permissionsManyListFormatter(item.custom_permissions).join(', ')}
@ -190,7 +190,7 @@ const CardUsers = ({
))}
{!loading && users.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</ul>

View File

@ -40,7 +40,7 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
<div className={`flex ${bgColor} ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} dark:bg-dark-900 border border-gray-600 items-center overflow-hidden`}>
<ImageField
name={'Avatar'}
name={'Аватар'}
image={item.avatar}
className='w-24 h-24 rounded-l overflow-hidden hidden md:block'
imageClassName={'rounded-l rounded-r-none h-full object-cover'}
@ -55,7 +55,7 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>First Name</p>
<p className={'text-xs text-gray-500 '}>Имя</p>
<p className={'line-clamp-2'}>{ item.firstName }</p>
</div>
@ -63,7 +63,7 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Last Name</p>
<p className={'text-xs text-gray-500 '}>Фамилия</p>
<p className={'line-clamp-2'}>{ item.lastName }</p>
</div>
@ -71,7 +71,7 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Phone Number</p>
<p className={'text-xs text-gray-500 '}>Телефон</p>
<p className={'line-clamp-2'}>{ item.phoneNumber }</p>
</div>
@ -79,7 +79,7 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>E-Mail</p>
<p className={'text-xs text-gray-500 '}>E-mail</p>
<p className={'line-clamp-2'}>{ item.email }</p>
</div>
@ -87,7 +87,7 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Disabled</p>
<p className={'text-xs text-gray-500 '}>Отключён</p>
<p className={'line-clamp-2'}>{ dataFormatter.booleanFormatter(item.disabled) }</p>
</div>
@ -95,9 +95,9 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Avatar</p>
<p className={'text-xs text-gray-500 '}>Аватар</p>
<ImageField
name={'Avatar'}
name={'Аватар'}
image={item.avatar}
className='mx-auto w-8 h-8'
/>
@ -107,7 +107,7 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>App Role</p>
<p className={'text-xs text-gray-500 '}>Роль приложения</p>
<p className={'line-clamp-2'}>{ dataFormatter.rolesOneListFormatter(item.app_role) }</p>
</div>
@ -115,7 +115,7 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Custom Permissions</p>
<p className={'text-xs text-gray-500 '}>Индивидуальные права</p>
<p className={'line-clamp-2'}>{ dataFormatter.permissionsManyListFormatter(item.custom_permissions).join(', ')}</p>
</div>
@ -137,7 +137,7 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
))}
{!loading && users.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<p className=''>Нет данных для отображения</p>
</div>
)}
</div>

View File

@ -16,6 +16,7 @@ import {loadColumns} from "./configureUsersCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
import { dataGridRuLocaleText } from '../../helpers/dataGridRuLocale';
@ -210,6 +211,7 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
const dataGrid = (
<div id="usersTable" className='relative overflow-x-auto'>
<DataGrid
localeText={dataGridRuLocaleText}
autoHeight
rowHeight={64}
sx={dataGridStyles}
@ -277,7 +279,7 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
return (
<div key={filterItem.id} className="flex mb-4">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Filter</div>
<div className=" text-gray-500 font-bold">Фильтр</div>
<Field
className={controlClasses}
name='selectedField'
@ -301,7 +303,7 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
Значение
</div>
<Field
className={controlClasses}
@ -311,7 +313,7 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
<option value="">Выберите значение</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
@ -326,22 +328,22 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
)?.number ? (
<div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div>
<div className=" text-gray-500 font-bold">От</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div>
<div className=" text-gray-500 font-bold">До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
@ -356,12 +358,12 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
От
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
placeholder='От'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
@ -369,11 +371,11 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<div className=' text-gray-500 font-bold'>До</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
placeholder='До'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
@ -383,11 +385,11 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<div className=" text-gray-500 font-bold">Содержит</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
placeholder='Введите значение'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
@ -395,12 +397,12 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<div className=" text-gray-500 font-bold">Действие</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
label='Удалить'
onClick={() => {
deleteFilter(filterItem.id)
}}
@ -413,13 +415,13 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton
className="my-2 mr-3"
type='submit' color='info'
label='Apply'
label='Применить'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
type='reset' color='info' outline
label='Cancel'
label='Отмена'
onClick={handleReset}
/>
</div>
@ -429,9 +431,9 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
title="Подтвердите действие"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
buttonLabel={loading ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
@ -450,7 +452,7 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
label={`Удалить ${selectedRows.length === 1 ? 'строку' : 'строки'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),

View File

@ -43,7 +43,7 @@ export const loadColumns = async (
{
field: 'firstName',
headerName: 'First Name',
headerName: 'Имя',
flex: 1,
minWidth: 120,
filterable: false,
@ -58,7 +58,7 @@ export const loadColumns = async (
{
field: 'lastName',
headerName: 'Last Name',
headerName: 'Фамилия',
flex: 1,
minWidth: 120,
filterable: false,
@ -73,7 +73,7 @@ export const loadColumns = async (
{
field: 'phoneNumber',
headerName: 'Phone Number',
headerName: 'Телефон',
flex: 1,
minWidth: 120,
filterable: false,
@ -88,7 +88,7 @@ export const loadColumns = async (
{
field: 'email',
headerName: 'E-Mail',
headerName: 'E-mail',
flex: 1,
minWidth: 120,
filterable: false,
@ -103,7 +103,7 @@ export const loadColumns = async (
{
field: 'disabled',
headerName: 'Disabled',
headerName: 'Отключён',
flex: 1,
minWidth: 120,
filterable: false,
@ -119,7 +119,7 @@ export const loadColumns = async (
{
field: 'avatar',
headerName: 'Avatar',
headerName: 'Аватар',
flex: 1,
minWidth: 120,
filterable: false,
@ -130,7 +130,7 @@ export const loadColumns = async (
sortable: false,
renderCell: (params: GridValueGetterParams) => (
<ImageField
name={'Avatar'}
name={'Аватар'}
image={params?.row?.avatar}
className='w-24 h-24 mx-auto lg:w-6 lg:h-6'
/>
@ -140,7 +140,7 @@ export const loadColumns = async (
{
field: 'app_role',
headerName: 'App Role',
headerName: 'Роль приложения',
flex: 1,
minWidth: 120,
filterable: false,
@ -162,7 +162,7 @@ export const loadColumns = async (
{
field: 'custom_permissions',
headerName: 'Custom Permissions',
headerName: 'Индивидуальные права',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -67,7 +67,7 @@ export const WidgetCreator = ({
const errorMessage =
responcePayload.data?.error?.message || error?.message;
await dispatch(
setErrorNotification(errorMessage || 'Error with widget creation'),
setErrorNotification(errorMessage || 'Не удалось создать виджет'),
);
}
};
@ -90,11 +90,11 @@ export const WidgetCreator = ({
>
<Form>
<FormField
label='Create Chart or Widget'
label='Создать график или виджет'
help={
isFetchingQuery ?
'Loading...' :
'Describe your new widget or chart in natural language. For example: "Number of admin users" OR "red chart with number of closed contracts grouped by month"'
'Загрузка...' :
'Опишите новый виджет или график обычным языком. Например: "Количество администраторов" или "Красный график по числу закрытых контрактов по месяцам"'
}
>
<Field type='input' name='description' disabled={isFetchingQuery} />
@ -110,14 +110,14 @@ export const WidgetCreator = ({
>
{({ submitForm }) => (
<CardBoxModal
title='Widget Creator Settings'
title='Настройки генератора виджетов'
buttonColor='info'
buttonLabel='Done'
buttonLabel='Готово'
isActive={isModalOpen}
onConfirm={submitForm}
onCancel={() => setIsModalOpen(false)}
>
<p>What role are we showing and creating widgets for?</p>
<p>Для какой роли показывать и создавать виджеты?</p>
<Form>
<FormField>

View File

@ -8,8 +8,60 @@ export const localStorageStyleKey = 'style'
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
export const appTitle = 'created by Flatlogic generator!'
export const creatorBrandName = 'Neon. Vesper'
export const creatorWebsite = 'https://sites.google.com/view/books-nvesper/'
export const creatorIdeaAuthor = 'Печенька'
export const creatorCoCreatorName = 'Лэсья'
export const platformName = 'Платформа AI-ролевых ботов'
export const appTitle = `${platformName}${creatorBrandName}`
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle}${appTitle}`
const pageTitleTranslations: Record<string, string> = {
Login: 'Вход',
Overview: 'Обзор',
Dashboard: 'Панель управления',
'Creator Studio': 'Студия создателя',
'AI Roleplay Studio': 'AI-студия ролевых игр',
'Privacy Policy': 'Политика конфиденциальности',
'Terms of Use': 'Условия использования',
'Create Persona': 'Создать личность',
'Create Bot': 'Создать бота',
'Edit profile': 'Профиль',
Users: 'Пользователи',
Roles: 'Роли',
Permissions: 'Права',
Personas: 'Личности',
Bots: 'Боты',
Conversations: 'Диалоги',
Messages: 'Сообщения',
Tags: 'Теги',
Bot_tags: 'Теги ботов',
User_search_logs: 'Логи поиска',
'View users': 'Просмотр пользователя',
'View roles': 'Просмотр роли',
'View permissions': 'Просмотр прав',
'View personas': 'Просмотр личности',
'View bots': 'Просмотр бота',
'View conversations': 'Просмотр диалога',
'View messages': 'Просмотр сообщения',
'Edit users': 'Редактировать пользователя',
'Edit roles': 'Редактировать роль',
'Edit permissions': 'Редактировать права',
'Edit personas': 'Редактировать личность',
'Edit bots': 'Редактировать бота',
'Edit conversations': 'Редактировать диалог',
'Edit messages': 'Редактировать сообщение',
'Verify Email': 'Подтверждение почты',
Registration: 'Регистрация',
'Forgot Password': 'Восстановление пароля',
'Set Password': 'Установить пароль',
'Reset Password': 'Сброс пароля',
'Search Result': 'Результаты поиска',
'New Item': 'Новый элемент',
Error: 'Ошибка',
Forms: 'Формы',
Tables: 'Таблицы',
}
export const getPageTitle = (currentPageTitle: string) => `${pageTitleTranslations[currentPageTitle] || currentPageTitle}${appTitle}`
export const tinyKey = process.env.NEXT_PUBLIC_TINY_KEY || ''

View File

@ -0,0 +1,51 @@
export const dataGridRuLocaleText = {
noRowsLabel: 'Нет данных',
noResultsOverlayLabel: 'Ничего не найдено',
errorOverlayDefaultLabel: 'Произошла ошибка.',
toolbarColumns: 'Столбцы',
toolbarFilters: 'Фильтры',
toolbarDensity: 'Плотность',
toolbarExport: 'Экспорт',
toolbarQuickFilterPlaceholder: 'Быстрый поиск…',
toolbarQuickFilterLabel: 'Поиск',
columnsPanelTextFieldLabel: 'Найти столбец',
columnsPanelShowAllButton: 'Показать все',
columnsPanelHideAllButton: 'Скрыть все',
filterPanelAddFilter: 'Добавить фильтр',
filterPanelDeleteIconLabel: 'Удалить',
filterPanelLogicOperator: 'Логический оператор',
filterPanelOperator: 'Оператор',
filterPanelOperatorAnd: 'И',
filterPanelOperatorOr: 'ИЛИ',
filterPanelColumns: 'Столбцы',
filterPanelInputLabel: 'Значение',
filterPanelInputPlaceholder: 'Введите значение',
filterOperatorContains: 'содержит',
filterOperatorEquals: 'равно',
filterOperatorStartsWith: 'начинается с',
filterOperatorEndsWith: 'заканчивается на',
filterOperatorIs: 'равно',
filterOperatorNot: 'не равно',
filterOperatorAfter: 'после',
filterOperatorOnOrAfter: 'в эту дату или после',
filterOperatorBefore: 'до',
filterOperatorOnOrBefore: 'в эту дату или раньше',
filterOperatorIsEmpty: 'пусто',
filterOperatorIsNotEmpty: 'не пусто',
filterOperatorIsAnyOf: 'любой из',
columnMenuLabel: 'Меню',
columnMenuShowColumns: 'Показать столбцы',
columnMenuFilter: 'Фильтр',
columnMenuHideColumn: 'Скрыть',
columnMenuUnsort: 'Сбросить сортировку',
columnMenuSortAsc: 'По возрастанию',
columnMenuSortDesc: 'По убыванию',
footerRowSelected: (count: number) =>
count === 1 ? 'Выбрана 1 строка' : `Выбрано строк: ${count}`,
footerTotalRows: 'Всего строк:',
footerPaginationRowsPerPage: 'Строк на странице:',
paginationDisplayedRows: ({ from, to, count }: { from: number; to: number; count: number }) =>
`${from}${to} из ${count === -1 ? `более чем ${to}` : count}`,
};
export default dataGridRuLocaleText;

View File

@ -1,11 +1,97 @@
export function humanize(str: string) {
if (!str) {
return '';
}
return str.toString()
.replace(/^[\s_]+|[\s_]+$/g, '')
.replace(/[_\s]+/g, ' ')
.replace(/^[a-z]/, function (m) {
return m.toUpperCase();
});
const translations: Record<string, string> = {
users: 'Пользователи',
roles: 'Роли',
permissions: 'Права',
personas: 'Личности',
bots: 'Боты',
tags: 'Теги',
bottags: 'Теги ботов',
conversations: 'Диалоги',
messages: 'Сообщения',
usersearchlogs: 'Логи поиска',
author: 'Автор',
user: 'Пользователь',
bot: 'Бот',
tag: 'Тег',
persona: 'Личность',
name: 'Название',
botname: 'Имя бота',
personaname: 'Имя личности',
tagname: 'Имя тега',
avatar: 'Аватар',
backstory: 'Предыстория',
greeting: 'Приветствие',
description: 'Описание',
descriptionprompt: 'Описание / промпт',
visibility: 'Видимость',
draft: 'Черновик',
birthdate: 'Дата рождения',
appearance: 'Внешность',
heightweight: 'Рост / вес',
age: 'Возраст',
activepersona: 'Активная личность',
conversation: 'Диалог',
conversationtitle: 'Название диалога',
role: 'Роль',
content: 'Содержимое',
sentat: 'Отправлено',
tokencount: 'Токены',
startedat: 'Начало',
lastmessageat: 'Последнее сообщение',
status: 'Статус',
firstname: 'Имя',
lastname: 'Фамилия',
phonenumber: 'Телефон',
email: 'E-mail',
approle: 'Роль приложения',
custompermissions: 'Индивидуальные права',
bottagsbot: 'Теги этого бота',
bottagstag: 'Связи тега с ботами',
conversationsbot: 'Диалоги бота',
conversationspersona: 'Диалоги личности',
conversationsuser: 'Диалоги пользователя',
messagesconversation: 'Сообщения диалога',
personasuser: 'Личности пользователя',
botsauthor: 'Боты автора',
usersearchlogsuser: 'Логи поиска пользователя',
usersapprole: 'Пользователи роли',
query: 'Запрос',
scope: 'Область поиска',
slug: 'Слаг',
nsfw: 'NSFW',
};
function normalizeKey(value: string) {
return value.toLowerCase().replace(/[^a-z0-9а-яё]/gi, '');
}
export function humanize(str: string) {
if (!str) {
return '';
}
const raw = str.toString().trim();
const normalized = normalizeKey(raw);
if (translations[raw]) {
return translations[raw];
}
if (translations[normalized]) {
return translations[normalized];
}
const prepared = raw
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/([А-ЯЁ])([А-ЯЁ][а-яё])/g, '$1 $2')
.replace(/^[\s_]+|[\s_]+$/g, '')
.replace(/[_\s]+/g, ' ')
.trim();
const preparedNormalized = normalizeKey(prepared);
if (translations[preparedNormalized]) {
return translations[preparedNormalized];
}
return prepared.replace(/^[a-zа-яё]/i, (m) => m.toUpperCase());
}

View File

@ -0,0 +1,31 @@
type NameValidationOptions = {
label: string;
maxLength?: number;
required?: boolean;
};
export const universalAlphabetNamePattern = /^[\p{L}\p{M}]+(?:[ -][\p{L}\p{M}]+)*$/u;
export const universalAlphabetNameHelp =
'Используй буквы любого алфавита. Между словами допустимы пробелы и дефисы.';
export function validateUniversalAlphabetName(
value: string,
{ label, maxLength = 100, required = true }: NameValidationOptions,
) {
const normalizedValue = typeof value === 'string' ? value.trim() : '';
if (!normalizedValue) {
return required ? `Укажи ${label.toLowerCase()}.` : '';
}
if (normalizedValue.length > maxLength) {
return `${label} должно быть не длиннее ${maxLength} символов.`;
}
if (!universalAlphabetNamePattern.test(normalizedValue)) {
return `${label} может содержать только буквы любого алфавита. Между словами допустимы пробелы и дефисы.`;
}
return '';
}

View File

@ -18,9 +18,9 @@ export const rejectNotify = (state, action) => {
state.notify.textNotification = msg;
} else {
state.notify.textNotification = 'Network error';
state.notify.textNotification = 'Ошибка сети';
}
state.notify.textNotification = state.notify.textNotification || 'Network error';
state.notify.textNotification = state.notify.textNotification || 'Ошибка сети';
state.notify.typeNotification = 'error';
state.notify.showNotification = true;
};

View File

@ -1,5 +1,4 @@
import React, { ReactNode, useEffect } from 'react'
import { useState } from 'react'
import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'
@ -122,7 +121,7 @@ export default function LayoutAuthenticated({
onAsideLgClose={() => setIsAsideLgActive(false)}
/>
{children}
<FooterBar>Hand-crafted & Made with </FooterBar>
<FooterBar>Сделано с для ролевых историй</FooterBar>
</div>
</div>
)

View File

@ -5,12 +5,18 @@ const menuAside: MenuAsideItem[] = [
{
href: '/dashboard',
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
label: 'Панель управления',
},
{
href: '/creator-studio',
label: 'Студия создателя',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiRobot' in icon ? icon['mdiRobot' as keyof typeof icon] : icon.mdiViewDashboardOutline,
},
{
href: '/users/users-list',
label: 'Users',
label: 'Пользователи',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiAccountGroup ?? icon.mdiTable,
@ -18,7 +24,7 @@ const menuAside: MenuAsideItem[] = [
},
{
href: '/roles/roles-list',
label: 'Roles',
label: 'Роли',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
@ -26,7 +32,7 @@ const menuAside: MenuAsideItem[] = [
},
{
href: '/permissions/permissions-list',
label: 'Permissions',
label: 'Права',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
@ -34,7 +40,7 @@ const menuAside: MenuAsideItem[] = [
},
{
href: '/personas/personas-list',
label: 'Personas',
label: 'Личности',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiAccountStar' in icon ? icon['mdiAccountStar' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -42,7 +48,7 @@ const menuAside: MenuAsideItem[] = [
},
{
href: '/bots/bots-list',
label: 'Bots',
label: 'Боты',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiRobot' in icon ? icon['mdiRobot' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -50,7 +56,7 @@ const menuAside: MenuAsideItem[] = [
},
{
href: '/tags/tags-list',
label: 'Tags',
label: 'Теги',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiTag' in icon ? icon['mdiTag' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -58,7 +64,7 @@ const menuAside: MenuAsideItem[] = [
},
{
href: '/bot_tags/bot_tags-list',
label: 'Bot tags',
label: 'Теги ботов',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiTagMultiple' in icon ? icon['mdiTagMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -66,7 +72,7 @@ const menuAside: MenuAsideItem[] = [
},
{
href: '/conversations/conversations-list',
label: 'Conversations',
label: 'Диалоги',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiForum' in icon ? icon['mdiForum' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -74,7 +80,7 @@ const menuAside: MenuAsideItem[] = [
},
{
href: '/messages/messages-list',
label: 'Messages',
label: 'Сообщения',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiMessageText' in icon ? icon['mdiMessageText' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -82,7 +88,7 @@ const menuAside: MenuAsideItem[] = [
},
{
href: '/user_search_logs/user_search_logs-list',
label: 'User search logs',
label: 'Логи поиска',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiMagnify' in icon ? icon['mdiMagnify' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -90,11 +96,9 @@ const menuAside: MenuAsideItem[] = [
},
{
href: '/profile',
label: 'Profile',
label: 'Профиль',
icon: icon.mdiAccountCircle,
},
{
href: '/api-docs',
target: '_blank',

View File

@ -1,16 +1,4 @@
import {
mdiMenu,
mdiClockOutline,
mdiCloud,
mdiCrop,
mdiAccount,
mdiCogOutline,
mdiEmail,
mdiLogout,
mdiThemeLightDark,
mdiGithub,
mdiVuejs,
} from '@mdi/js'
import { mdiAccount, mdiLogout, mdiThemeLightDark } from '@mdi/js'
import { MenuNavBarItem } from './interfaces'
const menuNavBar: MenuNavBarItem[] = [
@ -19,7 +7,7 @@ const menuNavBar: MenuNavBarItem[] = [
menu: [
{
icon: mdiAccount,
label: 'My Profile',
label: 'Мой профиль',
href: '/profile',
},
{
@ -27,27 +15,25 @@ const menuNavBar: MenuNavBarItem[] = [
},
{
icon: mdiLogout,
label: 'Log Out',
label: 'Выйти',
isLogout: true,
},
],
},
{
icon: mdiThemeLightDark,
label: 'Light/Dark',
label: 'Тема',
isDesktopNoLabel: true,
isToggleLightDark: true,
},
{
icon: mdiLogout,
label: 'Log out',
label: 'Выйти',
isDesktopNoLabel: true,
isLogout: true,
},
]
export const webPagesNavBar = [
];
export const webPagesNavBar = [];
export default menuNavBar

View File

@ -7,7 +7,7 @@ import { store } from '../stores/store';
import { Provider } from 'react-redux';
import '../css/main.css';
import axios from 'axios';
import { baseURLApi } from '../config';
import { appTitle, baseURLApi, creatorBrandName, creatorWebsite } from '../config';
import { useRouter } from 'next/router';
import ErrorBoundary from "../components/ErrorBoundary";
import DevModeBadge from '../components/DevModeBadge';
@ -149,9 +149,9 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
setStepsEnabled(false);
};
const title = 'AI Roleplay Bot Platform'
const description = "Platform for creating AI roleplay personas and chatbots with profiles, NSFW preferences, and global search."
const url = "https://flatlogic.com/"
const title = appTitle
const description = "Полноценное 18+ веб-приложение для AI-личностей, русскоязычных ботов, свободного поиска и мобильных roleplay-чатов."
const url = creatorWebsite
const image = "https://project-screens.s3.amazonaws.com/screenshots/39968/app-hero-20260512-152336.png"
const imageWidth = '1920'
const imageHeight = '960'
@ -162,9 +162,12 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<>
<Head>
<meta name="description" content={description} />
<meta name="author" content={creatorBrandName} />
<meta name="application-name" content={title} />
<meta name="theme-color" content="#050816" />
<meta property="og:url" content={url} />
<meta property="og:site_name" content="https://flatlogic.com/" />
<meta property="og:site_name" content={title} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
@ -179,7 +182,8 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<meta property="twitter:image:width" content={imageWidth} />
<meta property="twitter:image:height" content={imageHeight} />
<link rel="icon" href="/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.svg" />
</Head>
<ErrorBoundary>

View File

@ -133,10 +133,10 @@ const EditBot_tags = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit bot_tags')}</title>
<title>{getPageTitle('Редактировать связь тега и бота')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit bot_tags'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Редактировать связь тега и бота'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -168,7 +168,7 @@ const EditBot_tags = () => {
<FormField label='Bot' labelFor='bot'>
<FormField label='Бот' labelFor='bot'>
<Field
name='bot'
id='bot'
@ -232,7 +232,7 @@ const EditBot_tags = () => {
<FormField label='Tag' labelFor='tag'>
<FormField label='Тег' labelFor='tag'>
<Field
name='tag'
id='tag'
@ -279,9 +279,9 @@ const EditBot_tags = () => {
<BaseDivider />
<BaseButtons>
<BaseButton type="submit" color="info" label="Submit" />
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/bot_tags/bot_tags-list')}/>
<BaseButton type="submit" color="info" label="Сохранить" />
<BaseButton type="reset" color="info" outline label="Сбросить" />
<BaseButton type='reset' color='danger' outline label='Отмена' onClick={() => router.push('/bot_tags/bot_tags-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -130,10 +130,10 @@ const EditBot_tagsPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit bot_tags')}</title>
<title>{getPageTitle('Редактировать связь тега и бота')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit bot_tags'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Редактировать связь тега и бота'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -165,7 +165,7 @@ const EditBot_tagsPage = () => {
<FormField label='Bot' labelFor='bot'>
<FormField label='Бот' labelFor='bot'>
<Field
name='bot'
id='bot'
@ -229,7 +229,7 @@ const EditBot_tagsPage = () => {
<FormField label='Tag' labelFor='tag'>
<FormField label='Тег' labelFor='tag'>
<Field
name='tag'
id='tag'
@ -276,9 +276,9 @@ const EditBot_tagsPage = () => {
<BaseDivider />
<BaseButtons>
<BaseButton type="submit" color="info" label="Submit" />
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/bot_tags/bot_tags-list')}/>
<BaseButton type="submit" color="info" label="Сохранить" />
<BaseButton type="reset" color="info" outline label="Сбросить" />
<BaseButton type='reset' color='danger' outline label='Отмена' onClick={() => router.push('/bot_tags/bot_tags-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -40,11 +40,11 @@ const Bot_tagsTablesPage = () => {
{label: 'Bot', title: 'bot'},
{label: 'Бот', title: 'bot'},
{label: 'Tag', title: 'tag'},
{label: 'Тег', title: 'tag'},
@ -94,28 +94,28 @@ const Bot_tagsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Bot_tags')}</title>
<title>{getPageTitle('Теги ботов')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Bot_tags" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Теги ботов" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/bot_tags/bot_tags-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/bot_tags/bot_tags-new'} color='info' label='Новый элемент'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='Фильтр'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getBot_tagsCSV} />
<BaseButton className={'mr-3'} color='info' label='Скачать CSV' onClick={getBot_tagsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='Загрузить CSV'
onClick={() => setIsModalActive(true)}
/>
)}
@ -137,10 +137,10 @@ const Bot_tagsTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title='Загрузить CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
buttonLabel={'Подтвердить'}
// buttonLabel={false ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}

View File

@ -78,10 +78,10 @@ const Bot_tagsNew = () => {
return (
<>
<Head>
<title>{getPageTitle('New Item')}</title>
<title>{getPageTitle('Новый элемент')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Новый элемент" main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -115,7 +115,7 @@ const Bot_tagsNew = () => {
<FormField label="Bot" labelFor="bot">
<FormField label="Бот" labelFor="bot">
<Field name="bot" id="bot" component={SelectField} options={[]} itemRef={'bots'}></Field>
</FormField>
@ -145,7 +145,7 @@ const Bot_tagsNew = () => {
<FormField label="Tag" labelFor="tag">
<FormField label="Тег" labelFor="tag">
<Field name="tag" id="tag" component={SelectField} options={[]} itemRef={'tags'}></Field>
</FormField>
@ -157,9 +157,9 @@ const Bot_tagsNew = () => {
<BaseDivider />
<BaseButtons>
<BaseButton type="submit" color="info" label="Submit" />
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/bot_tags/bot_tags-list')}/>
<BaseButton type="submit" color="info" label="Сохранить" />
<BaseButton type="reset" color="info" outline label="Сбросить" />
<BaseButton type='reset' color='danger' outline label='Отмена' onClick={() => router.push('/bot_tags/bot_tags-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -40,11 +40,11 @@ const Bot_tagsTablesPage = () => {
{label: 'Bot', title: 'bot'},
{label: 'Бот', title: 'bot'},
{label: 'Tag', title: 'tag'},
{label: 'Тег', title: 'tag'},
@ -94,28 +94,28 @@ const Bot_tagsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Bot_tags')}</title>
<title>{getPageTitle('Теги ботов')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Bot_tags" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Теги ботов" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/bot_tags/bot_tags-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/bot_tags/bot_tags-new'} color='info' label='Новый элемент'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='Фильтр'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getBot_tagsCSV} />
<BaseButton className={'mr-3'} color='info' label='Скачать CSV' onClick={getBot_tagsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='Загрузить CSV'
onClick={() => setIsModalActive(true)}
/>
)}
@ -139,10 +139,10 @@ const Bot_tagsTablesPage = () => {
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
title='Загрузить CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
buttonLabel={'Подтвердить'}
// buttonLabel={false ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}

View File

@ -30,8 +30,7 @@ const Bot_tagsView = () => {
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
return str;
}
useEffect(() => {
@ -42,13 +41,13 @@ const Bot_tagsView = () => {
return (
<>
<Head>
<title>{getPageTitle('View bot_tags')}</title>
<title>{getPageTitle('Просмотр связи тега и бота')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View bot_tags')} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('Просмотр связи тега и бота')} main>
<BaseButton
color='info'
label='Edit'
label='Редактировать'
href={`/bot_tags/bot_tags-edit/?id=${id}`}
/>
</SectionTitleLineWithButton>
@ -76,7 +75,7 @@ const Bot_tagsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Bot</p>
<p className={'block font-bold mb-2'}>Бот</p>
@ -87,7 +86,7 @@ const Bot_tagsView = () => {
<p>{bot_tags?.bot?.name ?? 'No data'}</p>
<p>{bot_tags?.bot?.name ?? 'Нет данных'}</p>
@ -132,7 +131,7 @@ const Bot_tagsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Tag</p>
<p className={'block font-bold mb-2'}>Тег</p>
@ -145,7 +144,7 @@ const Bot_tagsView = () => {
<p>{bot_tags?.tag?.name ?? 'No data'}</p>
<p>{bot_tags?.tag?.name ?? 'Нет данных'}</p>
@ -183,7 +182,7 @@ const Bot_tagsView = () => {
<BaseButton
color='info'
label='Back'
label='Назад'
onClick={() => router.push('/bot_tags/bot_tags-list')}
/>
</CardBox>

View File

@ -329,10 +329,10 @@ const EditBots = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit bots')}</title>
<title>{getPageTitle('Редактировать бота')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit bots'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Редактировать бота'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -364,7 +364,7 @@ const EditBots = () => {
<FormField label='Author' labelFor='author'>
<FormField label='Автор' labelFor='author'>
<Field
name='author'
id='author'
@ -410,11 +410,13 @@ const EditBots = () => {
<FormField
label="BotName"
label="Имя бота"
help="Буквы любого алфавита, пробелы и дефисы между словами."
>
<Field
name="name"
placeholder="BotName"
placeholder="Имя бота"
maxLength={100}
/>
</FormField>
@ -470,7 +472,7 @@ const EditBots = () => {
<FormField>
<Field
label='Avatar'
label='Аватар'
color='info'
icon={mdiUpload}
path={'bots/avatar'}
@ -494,7 +496,7 @@ const EditBots = () => {
<FormField label='Backstory' hasTextareaHeight>
<FormField label='Предыстория' hasTextareaHeight>
<Field
name='backstory'
id='backstory'
@ -528,8 +530,8 @@ const EditBots = () => {
<FormField label="Greeting" hasTextareaHeight>
<Field name="greeting" as="textarea" placeholder="Greeting" />
<FormField label="Приветствие" hasTextareaHeight>
<Field name="greeting" as="textarea" placeholder="Приветствие" />
</FormField>
@ -562,7 +564,7 @@ const EditBots = () => {
<FormField label='Description/Prompt' hasTextareaHeight>
<FormField label='Описание / промпт' hasTextareaHeight>
<Field
name='description'
id='description'
@ -608,14 +610,14 @@ const EditBots = () => {
<FormField label="Visibility" labelFor="visibility">
<FormField label="Видимость" labelFor="visibility">
<Field name="visibility" id="visibility" component="select">
<option value="public">public</option>
<option value="public">Публичный</option>
<option value="unlisted">unlisted</option>
<option value="unlisted">По ссылке</option>
<option value="private">private</option>
<option value="private">Приватный</option>
</Field>
</FormField>
@ -686,7 +688,7 @@ const EditBots = () => {
<FormField label='Draft' labelFor='is_draft'>
<FormField label='Черновик' labelFor='is_draft'>
<Field
name='is_draft'
id='is_draft'
@ -708,9 +710,9 @@ const EditBots = () => {
<BaseDivider />
<BaseButtons>
<BaseButton type="submit" color="info" label="Submit" />
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/bots/bots-list')}/>
<BaseButton type="submit" color="info" label="Сохранить" />
<BaseButton type="reset" color="info" outline label="Сбросить" />
<BaseButton type='reset' color='danger' outline label='Отмена' onClick={() => router.push('/bots/bots-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -326,10 +326,10 @@ const EditBotsPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit bots')}</title>
<title>{getPageTitle('Редактировать бота')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit bots'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Редактировать бота'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -361,7 +361,7 @@ const EditBotsPage = () => {
<FormField label='Author' labelFor='author'>
<FormField label='Автор' labelFor='author'>
<Field
name='author'
id='author'
@ -407,11 +407,13 @@ const EditBotsPage = () => {
<FormField
label="BotName"
label="Имя бота"
help="Буквы любого алфавита, пробелы и дефисы между словами."
>
<Field
name="name"
placeholder="BotName"
placeholder="Имя бота"
maxLength={100}
/>
</FormField>
@ -467,7 +469,7 @@ const EditBotsPage = () => {
<FormField>
<Field
label='Avatar'
label='Аватар'
color='info'
icon={mdiUpload}
path={'bots/avatar'}
@ -491,7 +493,7 @@ const EditBotsPage = () => {
<FormField label='Backstory' hasTextareaHeight>
<FormField label='Предыстория' hasTextareaHeight>
<Field
name='backstory'
id='backstory'
@ -525,8 +527,8 @@ const EditBotsPage = () => {
<FormField label="Greeting" hasTextareaHeight>
<Field name="greeting" as="textarea" placeholder="Greeting" />
<FormField label="Приветствие" hasTextareaHeight>
<Field name="greeting" as="textarea" placeholder="Приветствие" />
</FormField>
@ -559,7 +561,7 @@ const EditBotsPage = () => {
<FormField label='Description/Prompt' hasTextareaHeight>
<FormField label='Описание / промпт' hasTextareaHeight>
<Field
name='description'
id='description'
@ -605,14 +607,14 @@ const EditBotsPage = () => {
<FormField label="Visibility" labelFor="visibility">
<FormField label="Видимость" labelFor="visibility">
<Field name="visibility" id="visibility" component="select">
<option value="public">public</option>
<option value="public">Публичный</option>
<option value="unlisted">unlisted</option>
<option value="unlisted">По ссылке</option>
<option value="private">private</option>
<option value="private">Приватный</option>
</Field>
</FormField>
@ -683,7 +685,7 @@ const EditBotsPage = () => {
<FormField label='Draft' labelFor='is_draft'>
<FormField label='Черновик' labelFor='is_draft'>
<Field
name='is_draft'
id='is_draft'
@ -705,9 +707,9 @@ const EditBotsPage = () => {
<BaseDivider />
<BaseButtons>
<BaseButton type="submit" color="info" label="Submit" />
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/bots/bots-list')}/>
<BaseButton type="submit" color="info" label="Сохранить" />
<BaseButton type="reset" color="info" outline label="Сбросить" />
<BaseButton type='reset' color='danger' outline label='Отмена' onClick={() => router.push('/bots/bots-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -34,17 +34,17 @@ const BotsTablesPage = () => {
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'BotName', title: 'name'},{label: 'Backstory', title: 'backstory'},{label: 'Greeting', title: 'greeting'},{label: 'Description/Prompt', title: 'description'},
const [filters] = useState([{label: 'Имя бота', title: 'name'},{label: 'Предыстория', title: 'backstory'},{label: 'Приветствие', title: 'greeting'},{label: 'Описание / промпт', title: 'description'},
{label: 'Author', title: 'author'},
{label: 'Автор', title: 'author'},
{label: 'Visibility', title: 'visibility', type: 'enum', options: ['public','unlisted','private']},
{label: 'Видимость', title: 'visibility', type: 'enum', options: ['public','unlisted','private']},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_BOTS');
@ -90,28 +90,28 @@ const BotsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Bots')}</title>
<title>{getPageTitle('Боты')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Bots" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Боты" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/bots/bots-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/bots/bots-new'} color='info' label='Новый элемент'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='Фильтр'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getBotsCSV} />
<BaseButton className={'mr-3'} color='info' label='Скачать CSV' onClick={getBotsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='Загрузить CSV'
onClick={() => setIsModalActive(true)}
/>
)}
@ -133,10 +133,10 @@ const BotsTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title='Загрузить CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
buttonLabel={'Подтвердить'}
// buttonLabel={false ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}

View File

@ -1,548 +1,261 @@
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js'
import Head from 'next/head'
import React, { ReactElement } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement, useState } from 'react';
import { Field, Form, Formik } from 'formik';
import { Field, Form, Formik } from 'formik'
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/bots/botsSlice'
import { useAppDispatch } from '../../stores/hooks'
import { useRouter } from 'next/router'
import moment from 'moment';
import BaseButton from '../../components/BaseButton';
import BaseDivider from '../../components/BaseDivider';
import CardBox from '../../components/CardBox';
import FormField from '../../components/FormField';
import FormImagePicker from '../../components/FormImagePicker';
import LoadingSpinner from '../../components/LoadingSpinner';
import { SwitchField } from '../../components/SwitchField';
import LayoutAuthenticated from '../../layouts/Authenticated';
import SectionMain from '../../components/SectionMain';
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
import { getPageTitle } from '../../config';
import { create } from '../../stores/bots/botsSlice';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { useRouter } from 'next/router';
import { hasPermission } from '../../helpers/userPermissions';
import { universalAlphabetNameHelp, validateUniversalAlphabetName } from '../../helpers/nameValidation';
const initialValues = {
author: '',
name: '',
avatar: [],
backstory: '',
greeting: '',
description: '',
visibility: 'public',
is_nsfw: false,
is_draft: false,
}
name: '',
avatar: [],
backstory: '',
greeting: '',
description: '',
visibility: 'public',
is_nsfw: false,
is_draft: false,
};
const BotsNew = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const router = useRouter();
const dispatch = useAppDispatch();
const { currentUser } = useAppSelector((state) => state.auth);
const [submitError, setSubmitError] = useState('');
const canCreateBots = hasPermission(currentUser, 'CREATE_BOTS');
const validate = (values: typeof initialValues) => {
const errors: Partial<Record<keyof typeof initialValues, string>> = {};
const trimmedName = values.name.trim();
const nameError = validateUniversalAlphabetName(trimmedName, {
label: 'Имя бота',
maxLength: 100,
});
const handleSubmit = async (data) => {
await dispatch(create(data))
await router.push('/bots/bots-list')
if (nameError) {
errors.name = nameError;
}
if (!values.is_draft) {
if (!values.backstory.trim()) {
errors.backstory = 'Предыстория обязательна для готового бота.';
}
if (!values.greeting.trim()) {
errors.greeting = 'Приветствие обязательно для готового бота.';
}
if (!values.description.trim()) {
errors.description = 'Описание / промпт обязательны для готового бота.';
}
}
return errors;
};
const handleSubmit = async (values: typeof initialValues) => {
if (!currentUser?.id) {
setSubmitError('Сначала нужно войти в аккаунт.');
return;
}
setSubmitError('');
try {
const payload = {
...values,
author: currentUser.id,
name: values.name.trim(),
backstory: values.backstory.trim(),
greeting: values.greeting.trim(),
description: values.description.trim(),
};
const createdBot = await dispatch(create(payload)).unwrap();
await router.push({
pathname: '/creator-studio',
query: {
created: 'bot',
...(createdBot?.id ? { focusBot: createdBot.id } : {}),
},
});
} catch (error: any) {
console.error('Не удалось создать бота:', error);
setSubmitError(error?.message || error || 'Сейчас не удалось создать бота.');
}
};
if (!currentUser) {
return (
<SectionMain>
<LoadingSpinner />
</SectionMain>
);
}
return (
<>
<Head>
<title>{getPageTitle('New Item')}</title>
<title>{getPageTitle('Create Bot')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
{''}
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title='Создать бота' main>
<BaseButton href='/creator-studio#my-bots' label='Назад в студию' color='whiteDark' />
</SectionTitleLineWithButton>
<CardBox>
<Formik
initialValues={
initialValues
}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label="Author" labelFor="author">
<Field name="author" id="author" component={SelectField} options={[]} itemRef={'users'}></Field>
</FormField>
<FormField
label="BotName"
>
<Field
name="name"
placeholder="BotName"
/>
</FormField>
<FormField>
<Field
label='Avatar'
color='info'
icon={mdiUpload}
path={'bots/avatar'}
name='avatar'
id='avatar'
schema={{
size: undefined,
formats: undefined,
}}
component={FormImagePicker}
></Field>
</FormField>
<FormField label='Backstory' hasTextareaHeight>
<Field
name='backstory'
id='backstory'
component={RichTextField}
></Field>
</FormField>
<FormField label="Greeting" hasTextareaHeight>
<Field name="greeting" as="textarea" placeholder="Greeting" />
</FormField>
<FormField label='Description/Prompt' hasTextareaHeight>
<Field
name='description'
id='description'
component={RichTextField}
></Field>
</FormField>
<FormField label="Visibility" labelFor="visibility">
<Field name="visibility" id="visibility" component="select">
<option value="public">public</option>
<option value="unlisted">unlisted</option>
<option value="private">private</option>
</Field>
</FormField>
<FormField label='NSFW' labelFor='is_nsfw'>
<Field
name='is_nsfw'
id='is_nsfw'
component={SwitchField}
></Field>
</FormField>
<FormField label='Draft' labelFor='is_draft'>
<Field
name='is_draft'
id='is_draft'
component={SwitchField}
></Field>
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type="submit" color="info" label="Submit" />
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/bots/bots-list')}/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
{!canCreateBots ? (
<CardBox>
<div className='text-lg font-semibold'>Нет доступа</div>
<p className='mt-2 text-sm text-slate-500 dark:text-slate-400'>
У тебя сейчас нет прав на создание ботов.
</p>
</CardBox>
) : (
<div className='grid gap-6 xl:grid-cols-[0.85fr,1.15fr]'>
<CardBox>
<div className='inline-flex items-center rounded-full border border-cyan-200 bg-cyan-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.24em] text-cyan-700 dark:border-cyan-500/20 dark:bg-cyan-500/10 dark:text-cyan-200'>
Шаг 2 · Конструктор бота
</div>
<h2 className='mt-4 text-2xl font-semibold'>Преврати идею в полноценного AI-персонажа</h2>
<p className='mt-3 text-sm leading-6 text-slate-500 dark:text-slate-400'>
Приветствие, предыстория и характер бота будут автоматически подмешиваться в AI-контекст при каждом ответе.
</p>
<div className='mt-6 rounded-3xl border border-slate-200 bg-white p-5 dark:border-dark-700 dark:bg-dark-800'>
<div className='text-sm font-semibold'>Что важно на старте</div>
<ul className='mt-3 space-y-2 text-sm leading-6 text-slate-500 dark:text-slate-400'>
<li> Имя бота буквы любого алфавита, до 100 символов.</li>
<li> Предыстория задаёт мир, отношения и факты.</li>
<li> Описание / промпт формирует тон, границы и характер.</li>
<li> Черновик позволяет сохранить основу и вернуться позже.</li>
</ul>
</div>
</CardBox>
<CardBox>
<Formik initialValues={initialValues} validate={validate} onSubmit={handleSubmit}>
{({ errors, touched, isSubmitting, values }) => (
<Form>
<div className='mb-6 flex items-start justify-between gap-4'>
<div>
<div className='text-sm uppercase tracking-[0.2em] text-slate-400'>Форма бота</div>
<h2 className='mt-2 text-2xl font-semibold'>Новый бот</h2>
<p className='mt-2 text-sm text-slate-500 dark:text-slate-400'>
После сохранения бот появится в студии создателя и будет участвовать в сортировке RU EN.
</p>
</div>
<div className='rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-600 dark:bg-dark-800 dark:text-slate-300'>
{values.name.trim().length}/100
</div>
</div>
{submitError && (
<div className='mb-6 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700 dark:border-rose-500/20 dark:bg-rose-500/10 dark:text-rose-200'>
{submitError}
</div>
)}
<FormField>
<Field
label='Аватар'
color='info'
icon={mdiUpload}
path={'bots/avatar'}
name='avatar'
id='avatar'
schema={{ size: undefined, formats: undefined }}
component={FormImagePicker}
/>
</FormField>
<FormField label='Имя бота' help={universalAlphabetNameHelp}>
<Field name='name' placeholder='Например: Серебряный Принц' maxLength={100} />
</FormField>
{touched.name && errors.name && <p className='-mt-4 mb-5 text-sm text-rose-500'>{errors.name}</p>}
<div className='grid gap-4 md:grid-cols-2'>
<div>
<FormField label='Видимость'>
<Field as='select' name='visibility'>
<option value='public'>Публичный</option>
<option value='unlisted'>По ссылке</option>
<option value='private'>Приватный</option>
</Field>
</FormField>
</div>
<div>
<FormField label='Сохранить как черновик'>
<Field name='is_draft' id='is_draft' component={SwitchField} />
</FormField>
</div>
</div>
<FormField label='Предыстория' help='Контекст мира, отношения между героями и важные факты.' hasTextareaHeight>
<Field as='textarea' name='backstory' placeholder='Например: наследник холодного северного двора, привыкший решать всё очарованием и угрозами.' maxLength={4000} />
</FormField>
{touched.backstory && errors.backstory && <p className='-mt-4 mb-5 text-sm text-rose-500'>{errors.backstory}</p>}
<FormField label='Приветствие' help='Первое сообщение, которое увидит пользователь в начале диалога.' hasTextareaHeight>
<Field as='textarea' name='greeting' placeholder='Например: принц поднимает взгляд с балкона и встречает тебя опасной улыбкой.' maxLength={2000} />
</FormField>
{touched.greeting && errors.greeting && <p className='-mt-4 mb-5 text-sm text-rose-500'>{errors.greeting}</p>}
<FormField label='Описание / промпт' help='Тон, границы сцены, стиль речи и то, как бот должен себя вести.' hasTextareaHeight>
<Field as='textarea' name='description' placeholder='Например: элегантный, дразнящий, политически расчётливый, но тайно заботливый.' maxLength={4000} />
</FormField>
{touched.description && errors.description && <p className='-mt-4 mb-5 text-sm text-rose-500'>{errors.description}</p>}
<div className='grid gap-4 md:grid-cols-2'>
<div>
<FormField label='Разрешить NSFW-режим'>
<Field name='is_nsfw' id='is_nsfw' component={SwitchField} />
</FormField>
</div>
<div className='rounded-3xl border border-slate-200 bg-slate-50 p-4 text-sm leading-6 text-slate-500 dark:border-dark-700 dark:bg-dark-800 dark:text-slate-400'>
Черновик позволяет сначала сохранить каркас бота, а детальные поля дописать позже.
</div>
</div>
<BaseDivider />
<div className='flex flex-wrap gap-3'>
<BaseButton type='submit' color='info' label={isSubmitting ? 'Сохраняем бота…' : values.is_draft ? 'Сохранить черновик' : 'Сохранить бота'} disabled={isSubmitting} />
<BaseButton href='/creator-studio#my-bots' color='whiteDark' label='Отмена' />
</div>
</Form>
)}
</Formik>
</CardBox>
</div>
)}
</SectionMain>
</>
)
}
);
};
BotsNew.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
<LayoutAuthenticated permission='CREATE_BOTS'>
{page}
</LayoutAuthenticated>
);
};
permission={'CREATE_BOTS'}
>
{page}
</LayoutAuthenticated>
)
}
export default BotsNew
export default BotsNew;

View File

@ -34,17 +34,17 @@ const BotsTablesPage = () => {
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'BotName', title: 'name'},{label: 'Backstory', title: 'backstory'},{label: 'Greeting', title: 'greeting'},{label: 'Description/Prompt', title: 'description'},
const [filters] = useState([{label: 'Имя бота', title: 'name'},{label: 'Предыстория', title: 'backstory'},{label: 'Приветствие', title: 'greeting'},{label: 'Описание / промпт', title: 'description'},
{label: 'Author', title: 'author'},
{label: 'Автор', title: 'author'},
{label: 'Visibility', title: 'visibility', type: 'enum', options: ['public','unlisted','private']},
{label: 'Видимость', title: 'visibility', type: 'enum', options: ['public','unlisted','private']},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_BOTS');
@ -90,28 +90,28 @@ const BotsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Bots')}</title>
<title>{getPageTitle('Боты')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Bots" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Боты" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/bots/bots-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/bots/bots-new'} color='info' label='Новый элемент'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='Фильтр'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getBotsCSV} />
<BaseButton className={'mr-3'} color='info' label='Скачать CSV' onClick={getBotsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='Загрузить CSV'
onClick={() => setIsModalActive(true)}
/>
)}
@ -135,10 +135,10 @@ const BotsTablesPage = () => {
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
title='Загрузить CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
buttonLabel={'Подтвердить'}
// buttonLabel={false ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}

View File

@ -30,8 +30,7 @@ const BotsView = () => {
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
return str;
}
useEffect(() => {
@ -42,13 +41,13 @@ const BotsView = () => {
return (
<>
<Head>
<title>{getPageTitle('View bots')}</title>
<title>{getPageTitle('Просмотр бота')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View bots')} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('Просмотр бота')} main>
<BaseButton
color='info'
label='Edit'
label='Редактировать'
href={`/bots/bots-edit/?id=${id}`}
/>
</SectionTitleLineWithButton>
@ -76,10 +75,10 @@ const BotsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Author</p>
<p className={'block font-bold mb-2'}>Автор</p>
<p>{bots?.author?.firstName ?? 'No data'}</p>
<p>{bots?.author?.firstName ?? 'Нет данных'}</p>
@ -113,7 +112,7 @@ const BotsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>BotName</p>
<p className={'block font-bold mb-2'}>Имя бота</p>
<p>{bots?.name}</p>
</div>
@ -167,7 +166,7 @@ const BotsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Avatar</p>
<p className={'block font-bold mb-2'}>Аватар</p>
{bots?.avatar?.length
? (
<ImageField
@ -175,7 +174,7 @@ const BotsView = () => {
image={bots?.avatar}
className='w-20 h-20'
/>
) : <p>No Avatar</p>
) : <p>Аватар не загружен</p>
}
</div>
@ -189,10 +188,10 @@ const BotsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Backstory</p>
<p className={'block font-bold mb-2'}>Предыстория</p>
{bots.backstory
? <p dangerouslySetInnerHTML={{__html: bots.backstory}}/>
: <p>No data</p>
: <p>Нет данных</p>
}
</div>
@ -221,7 +220,7 @@ const BotsView = () => {
<FormField label='Multi Text' hasTextareaHeight>
<FormField label='Многострочный текст' hasTextareaHeight>
<textarea className={'w-full'} disabled value={bots?.greeting} />
</FormField>
@ -255,10 +254,10 @@ const BotsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Description/Prompt</p>
<p className={'block font-bold mb-2'}>Описание / промпт</p>
{bots.description
? <p dangerouslySetInnerHTML={{__html: bots.description}}/>
: <p>No data</p>
: <p>Нет данных</p>
}
</div>
@ -300,8 +299,8 @@ const BotsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Visibility</p>
<p>{bots?.visibility ?? 'No data'}</p>
<p className={'block font-bold mb-2'}>Видимость</p>
<p>{bots?.visibility ?? 'Нет данных'}</p>
</div>
@ -368,7 +367,7 @@ const BotsView = () => {
<FormField label='Draft'>
<FormField label='Черновик'>
<SwitchField
field={{name: 'is_draft', value: bots?.is_draft}}
form={{setFieldValue: () => null}}
@ -394,7 +393,7 @@ const BotsView = () => {
<>
<p className={'block font-bold mb-2'}>Bot_tags Bot</p>
<p className={'block font-bold mb-2'}>Теги этого бота</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
@ -424,13 +423,13 @@ const BotsView = () => {
</tbody>
</table>
</div>
{!bots?.bot_tags_bot?.length && <div className={'text-center py-4'}>No data</div>}
{!bots?.bot_tags_bot?.length && <div className={'text-center py-4'}>Нет данных</div>}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Conversations Bot</p>
<p className={'block font-bold mb-2'}>Диалоги бота</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
@ -447,19 +446,19 @@ const BotsView = () => {
<th>ConversationTitle</th>
<th>Название диалога</th>
<th>Status</th>
<th>Статус</th>
<th>StartedAt</th>
<th>Начало</th>
<th>LastMessageAt</th>
<th>Последнее сообщение</th>
@ -514,7 +513,7 @@ const BotsView = () => {
</tbody>
</table>
</div>
{!bots?.conversations_bot?.length && <div className={'text-center py-4'}>No data</div>}
{!bots?.conversations_bot?.length && <div className={'text-center py-4'}>Нет данных</div>}
</CardBox>
</>
@ -526,7 +525,7 @@ const BotsView = () => {
<BaseButton
color='info'
label='Back'
label='Назад'
onClick={() => router.push('/bots/bots-list')}
/>
</CardBox>

View File

@ -301,10 +301,10 @@ const EditConversations = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit conversations')}</title>
<title>{getPageTitle('Редактировать диалог')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit conversations'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Редактировать диалог'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -336,7 +336,7 @@ const EditConversations = () => {
<FormField label='User' labelFor='user'>
<FormField label='Пользователь' labelFor='user'>
<Field
name='user'
id='user'
@ -400,7 +400,7 @@ const EditConversations = () => {
<FormField label='Bot' labelFor='bot'>
<FormField label='Бот' labelFor='bot'>
<Field
name='bot'
id='bot'
@ -464,7 +464,7 @@ const EditConversations = () => {
<FormField label='Persona' labelFor='persona'>
<FormField label='Личность' labelFor='persona'>
<Field
name='persona'
id='persona'
@ -510,11 +510,11 @@ const EditConversations = () => {
<FormField
label="ConversationTitle"
label="Название диалога"
>
<Field
name="title"
placeholder="ConversationTitle"
placeholder="Название диалога"
/>
</FormField>
@ -560,7 +560,7 @@ const EditConversations = () => {
<FormField label="Status" labelFor="status">
<FormField label="Статус" labelFor="status">
<Field name="status" id="status" component="select">
<option value="active">active</option>
@ -595,7 +595,7 @@ const EditConversations = () => {
<FormField
label="StartedAt"
label="Начало"
>
<DatePicker
dateFormat="yyyy-MM-dd hh:mm"
@ -638,7 +638,7 @@ const EditConversations = () => {
<FormField
label="LastMessageAt"
label="Последнее сообщение"
>
<DatePicker
dateFormat="yyyy-MM-dd hh:mm"
@ -708,9 +708,9 @@ const EditConversations = () => {
<BaseDivider />
<BaseButtons>
<BaseButton type="submit" color="info" label="Submit" />
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/conversations/conversations-list')}/>
<BaseButton type="submit" color="info" label="Сохранить" />
<BaseButton type="reset" color="info" outline label="Сбросить" />
<BaseButton type='reset' color='danger' outline label='Отмена' onClick={() => router.push('/conversations/conversations-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -298,10 +298,10 @@ const EditConversationsPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Edit conversations')}</title>
<title>{getPageTitle('Редактировать диалог')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit conversations'} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Редактировать диалог'} main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -333,7 +333,7 @@ const EditConversationsPage = () => {
<FormField label='User' labelFor='user'>
<FormField label='Пользователь' labelFor='user'>
<Field
name='user'
id='user'
@ -397,7 +397,7 @@ const EditConversationsPage = () => {
<FormField label='Bot' labelFor='bot'>
<FormField label='Бот' labelFor='bot'>
<Field
name='bot'
id='bot'
@ -461,7 +461,7 @@ const EditConversationsPage = () => {
<FormField label='Persona' labelFor='persona'>
<FormField label='Личность' labelFor='persona'>
<Field
name='persona'
id='persona'
@ -507,11 +507,11 @@ const EditConversationsPage = () => {
<FormField
label="ConversationTitle"
label="Название диалога"
>
<Field
name="title"
placeholder="ConversationTitle"
placeholder="Название диалога"
/>
</FormField>
@ -557,7 +557,7 @@ const EditConversationsPage = () => {
<FormField label="Status" labelFor="status">
<FormField label="Статус" labelFor="status">
<Field name="status" id="status" component="select">
<option value="active">active</option>
@ -592,7 +592,7 @@ const EditConversationsPage = () => {
<FormField
label="StartedAt"
label="Начало"
>
<DatePicker
dateFormat="yyyy-MM-dd hh:mm"
@ -635,7 +635,7 @@ const EditConversationsPage = () => {
<FormField
label="LastMessageAt"
label="Последнее сообщение"
>
<DatePicker
dateFormat="yyyy-MM-dd hh:mm"
@ -705,9 +705,9 @@ const EditConversationsPage = () => {
<BaseDivider />
<BaseButtons>
<BaseButton type="submit" color="info" label="Submit" />
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/conversations/conversations-list')}/>
<BaseButton type="submit" color="info" label="Сохранить" />
<BaseButton type="reset" color="info" outline label="Сбросить" />
<BaseButton type='reset' color='danger' outline label='Отмена' onClick={() => router.push('/conversations/conversations-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -34,25 +34,25 @@ const ConversationsTablesPage = () => {
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'ConversationTitle', title: 'title'},
const [filters] = useState([{label: 'Название диалога', title: 'title'},
{label: 'StartedAt', title: 'started_at', date: 'true'},{label: 'LastMessageAt', title: 'last_message_at', date: 'true'},
{label: 'Начало', title: 'started_at', date: 'true'},{label: 'Последнее сообщение', title: 'last_message_at', date: 'true'},
{label: 'User', title: 'user'},
{label: 'Пользователь', title: 'user'},
{label: 'Bot', title: 'bot'},
{label: 'Бот', title: 'bot'},
{label: 'Persona', title: 'persona'},
{label: 'Личность', title: 'persona'},
{label: 'Status', title: 'status', type: 'enum', options: ['active','archived']},
{label: 'Статус', title: 'status', type: 'enum', options: ['active','archived']},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_CONVERSATIONS');
@ -98,28 +98,28 @@ const ConversationsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Conversations')}</title>
<title>{getPageTitle('Диалоги')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Conversations" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Диалоги" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/conversations/conversations-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/conversations/conversations-new'} color='info' label='Новый элемент'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='Фильтр'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getConversationsCSV} />
<BaseButton className={'mr-3'} color='info' label='Скачать CSV' onClick={getConversationsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='Загрузить CSV'
onClick={() => setIsModalActive(true)}
/>
)}
@ -129,7 +129,7 @@ const ConversationsTablesPage = () => {
</div>
<div className='md:inline-flex items-center ms-auto'>
<Link href={'/conversations/conversations-table'}>Switch to Table</Link>
<Link href={'/conversations/conversations-table'}>Открыть таблицу</Link>
</div>
</CardBox>
@ -145,10 +145,10 @@ const ConversationsTablesPage = () => {
</SectionMain>
<CardBoxModal
title='Upload CSV'
title='Загрузить CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
buttonLabel={'Подтвердить'}
// buttonLabel={false ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}

View File

@ -178,10 +178,10 @@ const ConversationsNew = () => {
return (
<>
<Head>
<title>{getPageTitle('New Item')}</title>
<title>{getPageTitle('Новый элемент')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Новый элемент" main>
{''}
</SectionTitleLineWithButton>
<CardBox>
@ -222,7 +222,7 @@ const ConversationsNew = () => {
<FormField label="User" labelFor="user">
<FormField label="Пользователь" labelFor="user">
<Field name="user" id="user" component={SelectField} options={[]} itemRef={'users'}></Field>
</FormField>
@ -252,7 +252,7 @@ const ConversationsNew = () => {
<FormField label="Bot" labelFor="bot">
<FormField label="Бот" labelFor="bot">
<Field name="bot" id="bot" component={SelectField} options={[]} itemRef={'bots'}></Field>
</FormField>
@ -282,7 +282,7 @@ const ConversationsNew = () => {
<FormField label="Persona" labelFor="persona">
<FormField label="Личность" labelFor="persona">
<Field name="persona" id="persona" component={SelectField} options={[]} itemRef={'personas'}></Field>
</FormField>
@ -295,11 +295,11 @@ const ConversationsNew = () => {
<FormField
label="ConversationTitle"
label="Название диалога"
>
<Field
name="title"
placeholder="ConversationTitle"
placeholder="Название диалога"
/>
</FormField>
@ -343,7 +343,7 @@ const ConversationsNew = () => {
<FormField label="Status" labelFor="status">
<FormField label="Статус" labelFor="status">
<Field name="status" id="status" component="select">
<option value="active">active</option>
@ -376,12 +376,12 @@ const ConversationsNew = () => {
<FormField
label="StartedAt"
label="Начало"
>
<Field
type="datetime-local"
name="started_at"
placeholder="StartedAt"
placeholder="Начало"
/>
</FormField>
@ -412,12 +412,12 @@ const ConversationsNew = () => {
<FormField
label="LastMessageAt"
label="Последнее сообщение"
>
<Field
type="datetime-local"
name="last_message_at"
placeholder="LastMessageAt"
placeholder="Последнее сообщение"
/>
</FormField>
@ -471,9 +471,9 @@ const ConversationsNew = () => {
<BaseDivider />
<BaseButtons>
<BaseButton type="submit" color="info" label="Submit" />
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/conversations/conversations-list')}/>
<BaseButton type="submit" color="info" label="Сохранить" />
<BaseButton type="reset" color="info" outline label="Сбросить" />
<BaseButton type='reset' color='danger' outline label='Отмена' onClick={() => router.push('/conversations/conversations-list')}/>
</BaseButtons>
</Form>
</Formik>

View File

@ -34,25 +34,25 @@ const ConversationsTablesPage = () => {
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'ConversationTitle', title: 'title'},
const [filters] = useState([{label: 'Название диалога', title: 'title'},
{label: 'StartedAt', title: 'started_at', date: 'true'},{label: 'LastMessageAt', title: 'last_message_at', date: 'true'},
{label: 'Начало', title: 'started_at', date: 'true'},{label: 'Последнее сообщение', title: 'last_message_at', date: 'true'},
{label: 'User', title: 'user'},
{label: 'Пользователь', title: 'user'},
{label: 'Bot', title: 'bot'},
{label: 'Бот', title: 'bot'},
{label: 'Persona', title: 'persona'},
{label: 'Личность', title: 'persona'},
{label: 'Status', title: 'status', type: 'enum', options: ['active','archived']},
{label: 'Статус', title: 'status', type: 'enum', options: ['active','archived']},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_CONVERSATIONS');
@ -98,28 +98,28 @@ const ConversationsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Conversations')}</title>
<title>{getPageTitle('Диалоги')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Conversations" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Диалоги" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/conversations/conversations-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/conversations/conversations-new'} color='info' label='Новый элемент'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='Фильтр'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getConversationsCSV} />
<BaseButton className={'mr-3'} color='info' label='Скачать CSV' onClick={getConversationsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='Загрузить CSV'
onClick={() => setIsModalActive(true)}
/>
)}
@ -143,10 +143,10 @@ const ConversationsTablesPage = () => {
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
title='Загрузить CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
buttonLabel={'Подтвердить'}
// buttonLabel={false ? 'Удаляем...' : 'Подтвердить'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}

View File

@ -30,8 +30,7 @@ const ConversationsView = () => {
const { id } = router.query;
function removeLastCharacter(str) {
console.log(str,`str`)
return str.slice(0, -1);
return str;
}
useEffect(() => {
@ -42,13 +41,13 @@ const ConversationsView = () => {
return (
<>
<Head>
<title>{getPageTitle('View conversations')}</title>
<title>{getPageTitle('Просмотр диалога')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View conversations')} main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('Просмотр диалога')} main>
<BaseButton
color='info'
label='Edit'
label='Редактировать'
href={`/conversations/conversations-edit/?id=${id}`}
/>
</SectionTitleLineWithButton>
@ -76,10 +75,10 @@ const ConversationsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>User</p>
<p className={'block font-bold mb-2'}>Пользователь</p>
<p>{conversations?.user?.firstName ?? 'No data'}</p>
<p>{conversations?.user?.firstName ?? 'Нет данных'}</p>
@ -132,7 +131,7 @@ const ConversationsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Bot</p>
<p className={'block font-bold mb-2'}>Бот</p>
@ -143,7 +142,7 @@ const ConversationsView = () => {
<p>{conversations?.bot?.name ?? 'No data'}</p>
<p>{conversations?.bot?.name ?? 'Нет данных'}</p>
@ -188,7 +187,7 @@ const ConversationsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Persona</p>
<p className={'block font-bold mb-2'}>Личность</p>
@ -197,7 +196,7 @@ const ConversationsView = () => {
<p>{conversations?.persona?.name ?? 'No data'}</p>
<p>{conversations?.persona?.name ?? 'Нет данных'}</p>
@ -225,7 +224,7 @@ const ConversationsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>ConversationTitle</p>
<p className={'block font-bold mb-2'}>Название диалога</p>
<p>{conversations?.title}</p>
</div>
@ -271,8 +270,8 @@ const ConversationsView = () => {
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Status</p>
<p>{conversations?.status ?? 'No data'}</p>
<p className={'block font-bold mb-2'}>Статус</p>
<p>{conversations?.status ?? 'Нет данных'}</p>
</div>
@ -298,7 +297,7 @@ const ConversationsView = () => {
<FormField label='StartedAt'>
<FormField label='Начало'>
{conversations.started_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
@ -308,7 +307,7 @@ const ConversationsView = () => {
) : null
}
disabled
/> : <p>No StartedAt</p>}
/> : <p>Дата начала не указана</p>}
</FormField>
@ -338,7 +337,7 @@ const ConversationsView = () => {
<FormField label='LastMessageAt'>
<FormField label='Последнее сообщение'>
{conversations.last_message_at ? <DatePicker
dateFormat="yyyy-MM-dd hh:mm"
showTimeSelect
@ -348,7 +347,7 @@ const ConversationsView = () => {
) : null
}
disabled
/> : <p>No LastMessageAt</p>}
/> : <p>Дата последнего сообщения не указана</p>}
</FormField>
@ -412,7 +411,7 @@ const ConversationsView = () => {
<>
<p className={'block font-bold mb-2'}>Messages Conversation</p>
<p className={'block font-bold mb-2'}>Сообщения диалога</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
@ -425,19 +424,19 @@ const ConversationsView = () => {
<th>Role</th>
<th>Роль</th>
<th>Content</th>
<th>Содержимое</th>
<th>SentAt</th>
<th>Отправлено</th>
<th>TokenCount</th>
<th>Токены</th>
</tr>
@ -478,7 +477,7 @@ const ConversationsView = () => {
</tbody>
</table>
</div>
{!conversations?.messages_conversation?.length && <div className={'text-center py-4'}>No data</div>}
{!conversations?.messages_conversation?.length && <div className={'text-center py-4'}>Нет данных</div>}
</CardBox>
</>
@ -489,7 +488,7 @@ const ConversationsView = () => {
<BaseButton
color='info'
label='Back'
label='Назад'
onClick={() => router.push('/conversations/conversations-list')}
/>
</CardBox>

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@ const Dashboard = () => {
const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
const loadingMessage = 'Loading...';
const loadingMessage = 'Загрузка...';
const [users, setUsers] = React.useState(loadingMessage);
@ -90,13 +90,13 @@ const Dashboard = () => {
<>
<Head>
<title>
{getPageTitle('Overview')}
{getPageTitle('Обзор')}
</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant}
title='Overview'
title='Обзор'
main>
{''}
</SectionTitleLineWithButton>
@ -110,7 +110,7 @@ const Dashboard = () => {
{!!rolesWidgets.length &&
hasPermission(currentUser, 'CREATE_ROLES') && (
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
{`Виджеты роли: ${widgetsRole?.role?.label || 'Пользователи'}`}
</p>
)}
@ -124,7 +124,7 @@ const Dashboard = () => {
size={48}
path={icon.mdiLoading}
/>{' '}
Loading widgets...
Загрузка виджетов...
</div>
)}
@ -320,7 +320,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Bot tags
Tags ботов
</div>
<div className="text-3xl leading-tight font-semibold">
{bot_tags}

View File

@ -17,12 +17,12 @@ export default function Error() {
<SectionFullScreen bg="pinkRed">
<CardBox
className="w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12 shadow-2xl"
footer={<BaseButton href="/dashboard" label="Done" color="danger" />}
footer={<BaseButton href="/dashboard" label="Готово" color="danger" />}
>
<div className="space-y-3">
<h1 className="text-2xl">Unhandled exception</h1>
<h1 className="text-2xl">Непойманное исключение</h1>
<p>An Error Occurred</p>
<p>Произошла ошибка</p>
</div>
</CardBox>
</SectionFullScreen>

Some files were not shown because too many files have changed in this diff Show More