almost finish wizard

This commit is contained in:
Hesabix 2025-07-18 02:59:39 +00:00
parent 186229c848
commit fc2aa36b0e
17 changed files with 1957 additions and 556 deletions

View file

@ -0,0 +1,245 @@
<?php
namespace App\Controller;
use App\Entity\AIConversation;
use App\Entity\AIMessage;
use App\Service\Access;
use App\Service\Extractor;
use App\Service\Jdate;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
class AIConversationController extends AbstractController
{
#[Route('/api/ai/conversations/list', name: 'api_ai_conversations_list', methods: ['POST'])]
public function listConversations(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = $request->getPayload()->all();
$search = $params['search'] ?? '';
$category = $params['category'] ?? '';
$conversationRepo = $entityManager->getRepository(AIConversation::class);
if (!empty($search)) {
$conversations = $conversationRepo->searchByTitle($acc['user'], $acc['bid'], $search);
} elseif (!empty($category)) {
$conversations = $conversationRepo->findByCategory($acc['user'], $acc['bid'], $category);
} else {
$conversations = $conversationRepo->findActiveConversations($acc['user'], $acc['bid']);
}
$result = [];
foreach ($conversations as $conversation) {
$messageRepo = $entityManager->getRepository(AIMessage::class);
$lastMessage = $messageRepo->findLastMessageByConversation($conversation);
$stats = $messageRepo->getConversationStats($conversation);
$result[] = [
'id' => $conversation->getId(),
'title' => $conversation->getTitle(),
'category' => $conversation->getCategory(),
'createdAt' => $jdate->jdate('Y/m/d H:i', $conversation->getCreatedAt()),
'updatedAt' => $jdate->jdate('Y/m/d H:i', $conversation->getUpdatedAt()),
'messageCount' => count($conversation->getMessages()),
'lastMessage' => $lastMessage ? $lastMessage->getContent() : '',
'stats' => $stats
];
}
return $this->json($result);
}
#[Route('/api/ai/conversations/create', name: 'api_ai_conversations_create', methods: ['POST'])]
public function createConversation(
Request $request,
Access $access,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = $request->getPayload()->all();
$title = $params['title'] ?? 'گفتگوی جدید';
$category = $params['category'] ?? 'عمومی';
$conversation = new AIConversation();
$conversation->setUser($acc['user']);
$conversation->setBusiness($acc['bid']);
$conversation->setTitle($title);
$conversation->setCategory($category);
$entityManager->persist($conversation);
$entityManager->flush();
return $this->json([
'id' => $conversation->getId(),
'title' => $conversation->getTitle(),
'category' => $conversation->getCategory(),
'createdAt' => $conversation->getCreatedAt()
]);
}
#[Route('/api/ai/conversations/{id}/messages', name: 'api_ai_conversations_messages', methods: ['POST'])]
public function getConversationMessages(
int $id,
Access $access,
EntityManagerInterface $entityManager,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$conversation = $entityManager->getRepository(AIConversation::class)->find($id);
if (!$conversation) {
throw $this->createNotFoundException('گفتگو یافت نشد');
}
// بررسی دسترسی کاربر به این گفتگو
if ($conversation->getUser()->getId() !== $acc['user']->getId() ||
$conversation->getBusiness()->getId() !== $acc['bid']->getId()) {
throw $this->createAccessDeniedException();
}
$messageRepo = $entityManager->getRepository(AIMessage::class);
$messages = $messageRepo->findByConversation($conversation);
$result = [];
foreach ($messages as $message) {
$result[] = [
'id' => $message->getId(),
'role' => $message->getRole(),
'content' => $message->getContent(),
'createdAt' => $jdate->jdate('Y/m/d H:i', $message->getCreatedAt()),
'inputTokens' => $message->getInputTokens(),
'outputTokens' => $message->getOutputTokens(),
'inputCost' => $message->getInputCost(),
'outputCost' => $message->getOutputCost(),
'totalCost' => $message->getTotalCost(),
'model' => $message->getModel(),
'agentSource' => $message->getAgentSource()
];
}
return $this->json($result);
}
#[Route('/api/ai/conversations/{id}/update', name: 'api_ai_conversations_update', methods: ['POST'])]
public function updateConversation(
int $id,
Request $request,
Access $access,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$conversation = $entityManager->getRepository(AIConversation::class)->find($id);
if (!$conversation) {
throw $this->createNotFoundException('گفتگو یافت نشد');
}
// بررسی دسترسی کاربر به این گفتگو
if ($conversation->getUser()->getId() !== $acc['user']->getId() ||
$conversation->getBusiness()->getId() !== $acc['bid']->getId()) {
throw $this->createAccessDeniedException();
}
$params = $request->getPayload()->all();
if (isset($params['title'])) {
$conversation->setTitle($params['title']);
}
if (isset($params['category'])) {
$conversation->setCategory($params['category']);
}
$conversation->setUpdatedAt(time());
$entityManager->persist($conversation);
$entityManager->flush();
return $this->json(['success' => true]);
}
#[Route('/api/ai/conversations/{id}/delete', name: 'api_ai_conversations_delete', methods: ['POST'])]
public function deleteConversation(
int $id,
Access $access,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$conversation = $entityManager->getRepository(AIConversation::class)->find($id);
if (!$conversation) {
throw $this->createNotFoundException('گفتگو یافت نشد');
}
// بررسی دسترسی کاربر به این گفتگو
if ($conversation->getUser()->getId() !== $acc['user']->getId() ||
$conversation->getBusiness()->getId() !== $acc['bid']->getId()) {
throw $this->createAccessDeniedException();
}
// بررسی اینکه آیا گفتگو قبلاً حذف نشده است
if ($conversation->isDeleted()) {
return $this->json([
'success' => false,
'error' => 'این گفتگو قبلاً حذف شده است'
]);
}
// حذف نرم گفتگو (تنها علامت‌گذاری به عنوان حذف شده)
$conversationRepo = $entityManager->getRepository(AIConversation::class);
$conversationRepo->softDelete($conversation, true);
return $this->json(['success' => true]);
}
#[Route('/api/ai/conversations/categories', name: 'api_ai_conversations_categories', methods: ['POST'])]
public function getCategories(
Access $access,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$conversations = $entityManager->getRepository(AIConversation::class)
->findActiveConversations($acc['user'], $acc['bid']);
$categories = [];
foreach ($conversations as $conversation) {
$category = $conversation->getCategory();
if ($category && !in_array($category, $categories)) {
$categories[] = $category;
}
}
return $this->json($categories);
}
}

View file

@ -546,6 +546,7 @@ class BusinessController extends AbstractController
'plugGhestaManager' => true,
'plugTaxSettings' => true,
'inquiry' => true,
'ai' => true,
];
} elseif ($perm) {
$result = [
@ -591,6 +592,7 @@ class BusinessController extends AbstractController
'plugGhestaManager' => $perm->isPlugGhestaManager(),
'plugTaxSettings' => $perm->isPlugTaxSettings(),
'inquiry' => $perm->isInquiry(),
'ai' => $perm->isAi(),
];
}
return $this->json($result);
@ -662,6 +664,7 @@ class BusinessController extends AbstractController
$perm->setPlugGhestaManager($params['plugGhestaManager']);
$perm->setPlugTaxSettings($params['plugTaxSettings']);
$perm->setInquiry($params['inquiry']);
$perm->setAi($params['ai']);
$entityManager->persist($perm);
$entityManager->flush();
$log->insert('تنظیمات پایه', 'ویرایش دسترسی‌های کاربر با پست الکترونیکی ' . $user->getEmail(), $this->getUser(), $business);

View file

@ -1,12 +1,19 @@
<?php
namespace App\Controller;
use App\Entity\AIConversation;
use App\Entity\AIMessage;
use App\Service\AIService;
use App\Service\Access;
use App\Service\Extractor;
use App\Service\Log;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
class wizardController extends AbstractController
{
@ -18,9 +25,27 @@ class wizardController extends AbstractController
}
#[Route('/api/wizard/talk', name: 'wizard_talk', methods: ['POST'])]
public function wizard_talk(Request $request): JsonResponse
public function wizard_talk(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
Log $log
): JsonResponse
{
try {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
// بررسی دسترسی هوش مصنوعی
if (!$acc['ai']) {
return $this->json([
'success' => false,
'error' => 'شما دسترسی استفاده از هوش مصنوعی را ندارید'
]);
}
$params = json_decode($request->getContent(), true) ?? [];
if (!isset($params['message']) || empty($params['message'])) {
@ -32,6 +57,7 @@ class wizardController extends AbstractController
$message = $params['message'];
$options = $params['options'] ?? [];
$conversationId = $params['conversationId'] ?? null;
// بررسی فعال بودن هوش مصنوعی
if (!$this->aiService->isAIEnabled()) {
@ -41,28 +67,128 @@ class wizardController extends AbstractController
]);
}
// بررسی اعتبار کسب و کار
$business = $acc['bid'];
$currentBalance = (float) ($business->getSmsCharge() ?? 0);
// محاسبه هزینه تخمینی (حداقل 100 ریال)
$estimatedCost = 100;
if ($currentBalance < $estimatedCost) {
return $this->json([
'success' => false,
'error' => "اعتبار شما کافی نیست (اعتبار فعلی: {$currentBalance} ریال). برای شارژ حساب خود به بخش شارژ مراجعه کنید.",
'balance' => $currentBalance,
'required' => $estimatedCost,
'showChargeButton' => true
]);
}
// دریافت یا ایجاد گفتگو
$conversation = null;
if ($conversationId) {
$conversation = $entityManager->getRepository(AIConversation::class)->find($conversationId);
if (!$conversation ||
$conversation->getUser()->getId() !== $acc['user']->getId() ||
$conversation->getBusiness()->getId() !== $acc['bid']->getId()) {
return $this->json([
'success' => false,
'error' => 'گفتگو یافت نشد'
]);
}
} else {
// ایجاد گفتگوی جدید
$conversation = new AIConversation();
$conversation->setUser($acc['user']);
$conversation->setBusiness($acc['bid']);
$conversation->setTitle(substr($message, 0, 50) . '...');
$conversation->setCategory('عمومی');
$entityManager->persist($conversation);
}
// ذخیره پیام کاربر
$userMessage = new AIMessage();
$userMessage->setConversation($conversation);
$userMessage->setRole('user');
$userMessage->setContent($message);
$userMessage->setModel($this->aiService->getAIModel());
$userMessage->setAgentSource($this->aiService->getAIAgentSource());
$entityManager->persist($userMessage);
// ارسال درخواست به سرویس هوش مصنوعی
$result = $this->aiService->sendRequest($message, $options);
if ($result['success']) {
// ذخیره پاسخ هوش مصنوعی
$aiMessage = new AIMessage();
$aiMessage->setConversation($conversation);
$aiMessage->setRole('assistant');
$aiMessage->setContent($result['response']);
$aiMessage->setModel($result['model'] ?? $this->aiService->getAIModel());
$aiMessage->setAgentSource($this->aiService->getAIAgentSource());
// ذخیره اطلاعات usage
if (isset($result['usage'])) {
$aiMessage->setInputTokens($result['usage']['prompt_tokens'] ?? null);
$aiMessage->setOutputTokens($result['usage']['completion_tokens'] ?? null);
// محاسبه هزینه
$cost = $this->aiService->calculateCost($result['usage']);
$aiMessage->setInputCost($cost['input_cost'] ?? null);
$aiMessage->setOutputCost($cost['output_cost'] ?? null);
$aiMessage->setTotalCost($cost['total_cost'] ?? null);
}
$entityManager->persist($aiMessage);
// به‌روزرسانی زمان آخرین تغییر گفتگو
$conversation->setUpdatedAt(time());
$entityManager->persist($conversation);
$entityManager->flush();
$response = [
'success' => true,
'response' => $result['response'],
'conversationId' => $conversation->getId(),
'model' => $result['model'] ?? null,
'usage' => $result['usage'] ?? null
];
// محاسبه هزینه در صورت وجود اطلاعات usage
if (isset($result['usage'])) {
$cost = $this->aiService->calculateCost($result['usage']);
$response['cost'] = $cost;
// کسر اعتبار از کسب و کار
$totalCost = $cost['total_cost'] ?? 0;
if ($totalCost > 0) {
// گرد کردن هزینه به عدد صحیح
$totalCost = (int) round($totalCost);
$newBalance = (int) ($currentBalance - $totalCost);
$business->setSmsCharge((string) $newBalance);
$entityManager->persist($business);
// ثبت لاگ کسر اعتبار
$log->insert(
'هوش مصنوعی',
"کسر اعتبار برای استفاده از هوش مصنوعی: {$totalCost} ریال (اعتبار قبلی: {$currentBalance} ریال، اعتبار جدید: {$newBalance} ریال)",
$acc['user'],
$business
);
$entityManager->flush();
}
}
return $this->json($response);
} else {
// حذف پیام کاربر در صورت خطا
$entityManager->remove($userMessage);
$entityManager->flush();
return $this->json([
'success' => false,
'error' => $result['error']
'error' => $result['error'] ?? 'خطای نامشخص در سرویس هوش مصنوعی'
]);
}
@ -189,4 +315,37 @@ class wizardController extends AbstractController
]);
}
}
#[Route('/api/wizard/balance', name: 'wizard_balance', methods: ['GET'])]
public function wizard_balance(Access $access): JsonResponse
{
try {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
// بررسی دسترسی هوش مصنوعی
if (!$acc['ai']) {
return $this->json([
'success' => false,
'error' => 'شما دسترسی استفاده از هوش مصنوعی را ندارید'
]);
}
$business = $acc['bid'];
$balance = (float) ($business->getSmsCharge() ?? 0);
return $this->json([
'success' => true,
'balance' => $balance,
'formatted_balance' => number_format($balance, 0, '.', ',') . ' ریال'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'error' => 'خطا در دریافت اعتبار: ' . $e->getMessage()
]);
}
}
}

View file

@ -0,0 +1,174 @@
<?php
namespace App\Entity;
use App\Repository\AIConversationRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: AIConversationRepository::class)]
class AIConversation
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'aiConversations')]
#[ORM\JoinColumn(nullable: false)]
private ?User $user = null;
#[ORM\ManyToOne(inversedBy: 'aiConversations')]
#[ORM\JoinColumn(nullable: false)]
private ?Business $business = null;
#[ORM\Column(length: 255)]
private ?string $title = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $category = null;
#[ORM\Column]
private ?int $createdAt = null;
#[ORM\Column]
private ?int $updatedAt = null;
#[ORM\Column(nullable: true)]
private ?bool $isActive = true;
#[ORM\Column(nullable: true)]
private ?bool $deleted = false;
#[ORM\OneToMany(mappedBy: 'conversation', targetEntity: AIMessage::class, orphanRemoval: true)]
private Collection $messages;
public function __construct()
{
$this->messages = new ArrayCollection();
$this->createdAt = time();
$this->updatedAt = time();
}
public function getId(): ?int
{
return $this->id;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): static
{
$this->user = $user;
return $this;
}
public function getBusiness(): ?Business
{
return $this->business;
}
public function setBusiness(?Business $business): static
{
$this->business = $business;
return $this;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getCategory(): ?string
{
return $this->category;
}
public function setCategory(?string $category): static
{
$this->category = $category;
return $this;
}
public function getCreatedAt(): ?int
{
return $this->createdAt;
}
public function setCreatedAt(int $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?int
{
return $this->updatedAt;
}
public function setUpdatedAt(int $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
public function isActive(): ?bool
{
return $this->isActive;
}
public function setIsActive(?bool $isActive): static
{
$this->isActive = $isActive;
return $this;
}
public function isDeleted(): ?bool
{
return $this->deleted;
}
public function setDeleted(?bool $deleted): static
{
$this->deleted = $deleted;
return $this;
}
/**
* @return Collection<int, AIMessage>
*/
public function getMessages(): Collection
{
return $this->messages;
}
public function addMessage(AIMessage $message): static
{
if (!$this->messages->contains($message)) {
$this->messages->add($message);
$message->setConversation($this);
}
return $this;
}
public function removeMessage(AIMessage $message): static
{
if ($this->messages->removeElement($message)) {
// set the owning side to null (unless already changed)
if ($message->getConversation() === $this) {
$message->setConversation(null);
}
}
return $this;
}
}

View file

@ -0,0 +1,181 @@
<?php
namespace App\Entity;
use App\Repository\AIMessageRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: AIMessageRepository::class)]
class AIMessage
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'messages')]
#[ORM\JoinColumn(nullable: false)]
private ?AIConversation $conversation = null;
#[ORM\Column(length: 20)]
private ?string $role = null; // 'user' یا 'assistant'
#[ORM\Column(type: Types::TEXT)]
private ?string $content = null;
#[ORM\Column]
private ?int $createdAt = null;
#[ORM\Column(nullable: true)]
private ?int $inputTokens = null;
#[ORM\Column(nullable: true)]
private ?int $outputTokens = null;
#[ORM\Column(nullable: true)]
private ?float $inputCost = null;
#[ORM\Column(nullable: true)]
private ?float $outputCost = null;
#[ORM\Column(nullable: true)]
private ?float $totalCost = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $model = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $agentSource = null;
public function __construct()
{
$this->createdAt = time();
}
public function getId(): ?int
{
return $this->id;
}
public function getConversation(): ?AIConversation
{
return $this->conversation;
}
public function setConversation(?AIConversation $conversation): static
{
$this->conversation = $conversation;
return $this;
}
public function getRole(): ?string
{
return $this->role;
}
public function setRole(string $role): static
{
$this->role = $role;
return $this;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): static
{
$this->content = $content;
return $this;
}
public function getCreatedAt(): ?int
{
return $this->createdAt;
}
public function setCreatedAt(int $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getInputTokens(): ?int
{
return $this->inputTokens;
}
public function setInputTokens(?int $inputTokens): static
{
$this->inputTokens = $inputTokens;
return $this;
}
public function getOutputTokens(): ?int
{
return $this->outputTokens;
}
public function setOutputTokens(?int $outputTokens): static
{
$this->outputTokens = $outputTokens;
return $this;
}
public function getInputCost(): ?float
{
return $this->inputCost;
}
public function setInputCost(?float $inputCost): static
{
$this->inputCost = $inputCost;
return $this;
}
public function getOutputCost(): ?float
{
return $this->outputCost;
}
public function setOutputCost(?float $outputCost): static
{
$this->outputCost = $outputCost;
return $this;
}
public function getTotalCost(): ?float
{
return $this->totalCost;
}
public function setTotalCost(?float $totalCost): static
{
$this->totalCost = $totalCost;
return $this;
}
public function getModel(): ?string
{
return $this->model;
}
public function setModel(?string $model): static
{
$this->model = $model;
return $this;
}
public function getAgentSource(): ?string
{
return $this->agentSource;
}
public function setAgentSource(?string $agentSource): static
{
$this->agentSource = $agentSource;
return $this;
}
}

View file

@ -300,6 +300,9 @@ class Business
#[ORM\OneToMany(mappedBy: 'business', targetEntity: PlugHrmDoc::class)]
private Collection $plugHrmDocs;
#[ORM\OneToMany(mappedBy: 'business', targetEntity: AIConversation::class, orphanRemoval: true)]
private Collection $aiConversations;
public function __construct()
{
$this->logs = new ArrayCollection();
@ -343,6 +346,7 @@ class Business
$this->accountingPackageOrders = new ArrayCollection();
$this->PlugGhestaDocs = new ArrayCollection();
$this->plugHrmDocs = new ArrayCollection();
$this->aiConversations = new ArrayCollection();
}
public function getId(): ?int
@ -2086,4 +2090,34 @@ class Business
}
return $this;
}
/**
* @return Collection<int, AIConversation>
*/
public function getAiConversations(): Collection
{
return $this->aiConversations;
}
public function addAiConversation(AIConversation $aiConversation): static
{
if (!$this->aiConversations->contains($aiConversation)) {
$this->aiConversations->add($aiConversation);
$aiConversation->setBusiness($this);
}
return $this;
}
public function removeAiConversation(AIConversation $aiConversation): static
{
if ($this->aiConversations->removeElement($aiConversation)) {
// set the owning side to null (unless already changed)
if ($aiConversation->getBusiness() === $this) {
$aiConversation->setBusiness(null);
}
}
return $this;
}
}

View file

@ -135,6 +135,9 @@ class Permission
#[ORM\Column(nullable: true)]
private ?bool $inquiry = null;
#[ORM\Column(nullable: true)]
private ?bool $ai = null;
public function getId(): ?int
{
return $this->id;
@ -619,4 +622,16 @@ class Permission
return $this;
}
public function isAi(): ?bool
{
return $this->ai;
}
public function setAi(?bool $ai): static
{
$this->ai = $ai;
return $this;
}
}

View file

@ -137,6 +137,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\OneToMany(targetEntity: PlugGhestaDoc::class, mappedBy: 'submitter')]
private Collection $PlugGhestaDocs;
#[ORM\OneToMany(mappedBy: 'user', targetEntity: AIConversation::class, orphanRemoval: true)]
private Collection $aiConversations;
public function __construct()
{
$this->userTokens = new ArrayCollection();
@ -162,6 +165,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
$this->accountingPackageOrders = new ArrayCollection();
$this->backBuiltModules = new ArrayCollection();
$this->PlugGhestaDocs = new ArrayCollection();
$this->aiConversations = new ArrayCollection();
}
public function getId(): ?int
@ -997,4 +1001,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this;
}
/**
* @return Collection<int, AIConversation>
*/
public function getAiConversations(): Collection
{
return $this->aiConversations;
}
public function addAiConversation(AIConversation $aiConversation): static
{
if (!$this->aiConversations->contains($aiConversation)) {
$this->aiConversations->add($aiConversation);
$aiConversation->setUser($this);
}
return $this;
}
public function removeAiConversation(AIConversation $aiConversation): static
{
if ($this->aiConversations->removeElement($aiConversation)) {
// set the owning side to null (unless already changed)
if ($aiConversation->getUser() === $this) {
$aiConversation->setUser(null);
}
}
return $this;
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace App\Repository;
use App\Entity\AIConversation;
use App\Entity\Business;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<AIConversation>
*
* @method AIConversation|null find($id, $lockMode = null, $lockVersion = null)
* @method AIConversation|null findOneBy(array $criteria, array $orderBy = null)
* @method AIConversation[] findAll()
* @method AIConversation[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class AIConversationRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, AIConversation::class);
}
public function save(AIConversation $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(AIConversation $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* حذف نرم گفتگو (تنها علامت‌گذاری به عنوان حذف شده)
*/
public function softDelete(AIConversation $entity, bool $flush = false): void
{
$entity->setDeleted(true);
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* دریافت گفتگوهای کاربر در یک کسب و کار خاص
*/
public function findByUserAndBusiness(User $user, Business $business, bool $activeOnly = true): array
{
$criteria = [
'user' => $user,
'business' => $business,
'deleted' => false
];
if ($activeOnly) {
$criteria['isActive'] = true;
}
return $this->findBy($criteria, ['updatedAt' => 'DESC']);
}
/**
* دریافت گفتگوهای فعال کاربر در کسب و کار فعلی
*/
public function findActiveConversations(User $user, Business $business): array
{
return $this->findBy([
'user' => $user,
'business' => $business,
'isActive' => true,
'deleted' => false
], ['updatedAt' => 'DESC']);
}
/**
* جستجو در گفتگوها بر اساس عنوان
*/
public function searchByTitle(User $user, Business $business, string $searchTerm): array
{
$qb = $this->createQueryBuilder('c')
->where('c.user = :user')
->andWhere('c.business = :business')
->andWhere('c.isActive = :active')
->andWhere('c.deleted = :deleted')
->andWhere('c.title LIKE :search')
->setParameter('user', $user)
->setParameter('business', $business)
->setParameter('active', true)
->setParameter('deleted', false)
->setParameter('search', '%' . $searchTerm . '%')
->orderBy('c.updatedAt', 'DESC');
return $qb->getQuery()->getResult();
}
/**
* دریافت گفتگوها بر اساس دسته‌بندی
*/
public function findByCategory(User $user, Business $business, string $category): array
{
return $this->findBy([
'user' => $user,
'business' => $business,
'category' => $category,
'isActive' => true,
'deleted' => false
], ['updatedAt' => 'DESC']);
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace App\Repository;
use App\Entity\AIMessage;
use App\Entity\AIConversation;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<AIMessage>
*
* @method AIMessage|null find($id, $lockMode = null, $lockVersion = null)
* @method AIMessage|null findOneBy(array $criteria, array $orderBy = null)
* @method AIMessage[] findAll()
* @method AIMessage[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class AIMessageRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, AIMessage::class);
}
public function save(AIMessage $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(AIMessage $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* دریافت پیام‌های یک گفتگو
*/
public function findByConversation(AIConversation $conversation): array
{
return $this->findBy(['conversation' => $conversation], ['createdAt' => 'ASC']);
}
/**
* دریافت آخرین پیام یک گفتگو
*/
public function findLastMessageByConversation(AIConversation $conversation): ?AIMessage
{
return $this->findOneBy(['conversation' => $conversation], ['createdAt' => 'DESC']);
}
/**
* محاسبه آمار استفاده برای یک گفتگو
*/
public function getConversationStats(AIConversation $conversation): array
{
$qb = $this->createQueryBuilder('m')
->select('SUM(m.inputTokens) as totalInputTokens')
->addSelect('SUM(m.outputTokens) as totalOutputTokens')
->addSelect('SUM(m.inputCost) as totalInputCost')
->addSelect('SUM(m.outputCost) as totalOutputCost')
->addSelect('SUM(m.totalCost) as totalCost')
->where('m.conversation = :conversation')
->setParameter('conversation', $conversation);
$result = $qb->getQuery()->getSingleResult();
$totalInputTokens = $result['totalInputTokens'] ?? 0;
$totalOutputTokens = $result['totalOutputTokens'] ?? 0;
$totalTokens = $totalInputTokens + $totalOutputTokens;
return [
'totalInputTokens' => $totalInputTokens,
'totalOutputTokens' => $totalOutputTokens,
'totalTokens' => $totalTokens,
'totalInputCost' => $result['totalInputCost'] ?? 0,
'totalOutputCost' => $result['totalOutputCost'] ?? 0,
'totalCost' => $result['totalCost'] ?? 0,
];
}
}

View file

@ -52,6 +52,11 @@ class AIService
'success' => false,
'error' => 'خطا در ارتباط با سرویس هوش مصنوعی: ' . $e->getMessage()
];
} catch (\Throwable $e) {
return [
'success' => false,
'error' => 'خطای غیرمنتظره در سرویس هوش مصنوعی: ' . $e->getMessage()
];
}
}
@ -122,7 +127,7 @@ class AIService
return [
'success' => false,
'error' => $response['error'] ?? 'خطا در ارتباط با GapGPT'
'error' => $response['error'] ?? 'خطا در ارتباط با GapGPT: پاسخ نامعتبر'
];
}
@ -178,7 +183,7 @@ class AIService
return [
'success' => false,
'error' => $response['error'] ?? 'خطا در ارتباط با AvalAI'
'error' => $response['error'] ?? 'خطا در ارتباط با AvalAI: پاسخ نامعتبر'
];
}
@ -239,7 +244,7 @@ class AIService
return [
'success' => false,
'error' => $response['error'] ?? 'خطا در ارتباط با مدل لوکال'
'error' => $response['error'] ?? 'خطا در ارتباط با مدل لوکال: پاسخ نامعتبر'
];
}
@ -361,7 +366,7 @@ class AIService
/**
* محاسبه هزینه بر اساس تعداد توکن‌ها
*/
public function calculateCost(array $usage): float
public function calculateCost(array $usage): array
{
$inputTokenPrice = (float) ($this->registryMGR->get('system', 'inputTokenPrice') ?: 0);
$outputTokenPrice = (float) ($this->registryMGR->get('system', 'outputTokenPrice') ?: 0);
@ -371,7 +376,13 @@ class AIService
$inputCost = ($inputTokens / 1000) * $inputTokenPrice;
$outputCost = ($outputTokens / 1000) * $outputTokenPrice;
$totalCost = $inputCost + $outputCost;
return $inputCost + $outputCost;
// گرد کردن هزینه‌ها به اعداد صحیح
return [
'input_cost' => (int) round($inputCost),
'output_cost' => (int) round($outputCost),
'total_cost' => (int) round($totalCost)
];
}
}

View file

@ -96,8 +96,19 @@ class Access
'money'=>$money
];
// بررسی دسترسی هوش مصنوعی
$aiPermission = $this->em->getRepository(Permission::class)->findOneBy([
'bid'=>$bid,
'user'=>$this->user
]);
$accessArray['ai'] = false;
if($aiPermission && $aiPermission->isAi()){
$accessArray['ai'] = true;
}
if($bid->getOwner()->getEmail() === $this->user->getUserIdentifier()){
//user is owner
//user is owner - همه دسترسی‌ها را دارد
$accessArray['ai'] = true;
return $accessArray;
}
elseif ($this->user && $roll == 'join' && count($this->em->getRepository(Permission::class)->findBy(['user'=>$this->user,'bid'=>$bid]))){

View file

@ -17,6 +17,7 @@
"@tiptap/extension-text-align": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7",
"@tiptap/vue-3": "^2.11.7",
"@types/dompurify": "^3.0.5",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"@vueuse/core": "^13.1.0",
@ -27,12 +28,14 @@
"axios": "^1.8.4",
"date-fns": "^4.1.0",
"date-fns-jalali": "^3.2.0-0",
"dompurify": "^3.2.6",
"downloadjs": "^1.4.7",
"file-saver": "^2.0.5",
"html5-qrcode": "^2.3.8",
"jalali-moment": "^3.3.11",
"libphonenumber-js": "^1.12.7",
"lodash": "^4.17.21",
"marked": "^16.1.0",
"maska": "^3.1.1",
"maz-ui": "^3.50.1",
"pinia": "^3.0.2",

View file

@ -170,6 +170,7 @@ export default {
{ path: '/acc/business/logs', key: '=', label: this.$t('drawer.history'), ctrl: true, shift: true, permission: () => this.permissions.log },
{ path: '/acc/plugin/repservice/order/list', key: '[', label: this.$t('drawer.repservice_reqs'), ctrl: true, shift: true, permission: () => this.permissions.plugRepservice && this.isPluginActive('repservice') },
{ path: '/acc/inquiry/panel', key: ']', label: this.$t('drawer.inquiry'), ctrl: true, shift: true, permission: () => true },
{ path: '/acc/wizard/home', key: 'A', label: 'هوش مصنوعی', ctrl: true, shift: true, permission: () => this.permissions.ai },
{ path: '/acc/printers/list', key: ';', label: this.$t('drawer.cloud_printers'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/sms/panel', key: '`', label: this.$t('drawer.sms_panel'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/archive/list', key: '\'', label: this.$t('drawer.archive_files'), ctrl: true, shift: true, permission: () => this.permissions.archiveUpload || this.permissions.archiveMod || this.permissions.archiveDelete },
@ -798,6 +799,13 @@ export default {
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/inquiry/panel') }}</span>
</v-list-item-title>
</v-list-item>
<v-list-item v-if="permissions.ai" to="/acc/wizard/home">
<template v-slot:prepend><v-icon icon="mdi-robot"></v-icon></template>
<v-list-item-title>
هوش مصنوعی
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/wizard/home') }}</span>
</v-list-item-title>
</v-list-item>
<v-list-group v-show="isPluginActive('hrm') && permissions.plugHrmDocs">
<template v-slot:activator="{ props }">
<v-list-item class="text-dark" v-bind="props" :title="$t('drawer.hrm')">
@ -952,7 +960,7 @@ export default {
<span class="d-none d-sm-flex">{{ business.name }}</span>
</v-app-bar-title>
<v-spacer></v-spacer>
<v-tooltip text="جادوگر" location="bottom">
<v-tooltip text="هوش مصنوعی" location="bottom" v-if="permissions.ai">
<template v-slot:activator="{ props }">
<v-btn class="" stacked v-bind="props" to="/acc/wizard/home">
<v-icon>mdi-robot</v-icon>

View file

@ -174,6 +174,18 @@
:disabled="loadingSwitches.inquiry"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.ai"
label="هوش مصنوعی"
@change="savePerms('ai')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.ai"
:disabled="loadingSwitches.ai"
></v-switch>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
@ -719,7 +731,8 @@ export default {
plugHrmDocs: false,
plugGhestaManager: false,
plugTaxSettings: false,
inquiry: false
inquiry: false,
ai: false
};
axios.post('/api/business/get/user/permissions',

View file

@ -56,7 +56,6 @@
<ul class="mb-0 ps-4">
<li class="mb-2">به مبالغ انتخاب شده ۱۰ درصد مالیات بر ارزش افزوده اضافه میگردد.</li>
<li class="mb-2">اعتبار خریداری شده بلافاصله به حساب شما اضافه خواهد شد.</li>
<li>این اعتبار صرفاً برای استفاده از سرویس پیامک کوتاه قابل استفاده است و برای سایر خدمات قابل استفاده نمیباشد.</li>
</ul>
</div>
</v-alert>

File diff suppressed because it is too large Load diff