From fc2aa36b0e1c0e4904b4865bd245e9c6096a83d7 Mon Sep 17 00:00:00 2001 From: Babak Alizadeh Date: Fri, 18 Jul 2025 02:59:39 +0000 Subject: [PATCH] almost finish wizard --- .../Controller/AIConversationController.php | 245 +++ .../src/Controller/BusinessController.php | 3 + .../src/Controller/wizardController.php | 165 +- hesabixCore/src/Entity/AIConversation.php | 174 +++ hesabixCore/src/Entity/AIMessage.php | 181 +++ hesabixCore/src/Entity/Business.php | 34 + hesabixCore/src/Entity/Permission.php | 15 + hesabixCore/src/Entity/User.php | 34 + .../Repository/AIConversationRepository.php | 122 ++ .../src/Repository/AIMessageRepository.php | 88 ++ hesabixCore/src/Service/AIService.php | 21 +- hesabixCore/src/Service/Access.php | 13 +- webUI/package.json | 3 + webUI/src/views/acc/App.vue | 10 +- .../src/views/acc/settings/user_perm_edit.vue | 15 +- webUI/src/views/acc/smspanel/smspanel.vue | 1 - webUI/src/views/wizard/home.vue | 1389 ++++++++++------- 17 files changed, 1957 insertions(+), 556 deletions(-) create mode 100644 hesabixCore/src/Controller/AIConversationController.php create mode 100644 hesabixCore/src/Entity/AIConversation.php create mode 100644 hesabixCore/src/Entity/AIMessage.php create mode 100644 hesabixCore/src/Repository/AIConversationRepository.php create mode 100644 hesabixCore/src/Repository/AIMessageRepository.php diff --git a/hesabixCore/src/Controller/AIConversationController.php b/hesabixCore/src/Controller/AIConversationController.php new file mode 100644 index 00000000..31435124 --- /dev/null +++ b/hesabixCore/src/Controller/AIConversationController.php @@ -0,0 +1,245 @@ +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); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Controller/BusinessController.php b/hesabixCore/src/Controller/BusinessController.php index 3a31682b..01ff2689 100644 --- a/hesabixCore/src/Controller/BusinessController.php +++ b/hesabixCore/src/Controller/BusinessController.php @@ -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); diff --git a/hesabixCore/src/Controller/wizardController.php b/hesabixCore/src/Controller/wizardController.php index e8309dc8..c0d8a063 100644 --- a/hesabixCore/src/Controller/wizardController.php +++ b/hesabixCore/src/Controller/wizardController.php @@ -1,12 +1,19 @@ 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() + ]); + } + } } diff --git a/hesabixCore/src/Entity/AIConversation.php b/hesabixCore/src/Entity/AIConversation.php new file mode 100644 index 00000000..a543bab5 --- /dev/null +++ b/hesabixCore/src/Entity/AIConversation.php @@ -0,0 +1,174 @@ +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 + */ + 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; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/AIMessage.php b/hesabixCore/src/Entity/AIMessage.php new file mode 100644 index 00000000..b294f716 --- /dev/null +++ b/hesabixCore/src/Entity/AIMessage.php @@ -0,0 +1,181 @@ +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; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/Business.php b/hesabixCore/src/Entity/Business.php index b91d8085..85952313 100644 --- a/hesabixCore/src/Entity/Business.php +++ b/hesabixCore/src/Entity/Business.php @@ -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 + */ + 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; + } } diff --git a/hesabixCore/src/Entity/Permission.php b/hesabixCore/src/Entity/Permission.php index 1d0b4140..59550ab2 100644 --- a/hesabixCore/src/Entity/Permission.php +++ b/hesabixCore/src/Entity/Permission.php @@ -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; + } } diff --git a/hesabixCore/src/Entity/User.php b/hesabixCore/src/Entity/User.php index ccc7f5d3..630f9ea9 100644 --- a/hesabixCore/src/Entity/User.php +++ b/hesabixCore/src/Entity/User.php @@ -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 + */ + 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; + } } diff --git a/hesabixCore/src/Repository/AIConversationRepository.php b/hesabixCore/src/Repository/AIConversationRepository.php new file mode 100644 index 00000000..f322bb41 --- /dev/null +++ b/hesabixCore/src/Repository/AIConversationRepository.php @@ -0,0 +1,122 @@ + + * + * @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']); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Repository/AIMessageRepository.php b/hesabixCore/src/Repository/AIMessageRepository.php new file mode 100644 index 00000000..277dcbb0 --- /dev/null +++ b/hesabixCore/src/Repository/AIMessageRepository.php @@ -0,0 +1,88 @@ + + * + * @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, + ]; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Service/AIService.php b/hesabixCore/src/Service/AIService.php index 8076b210..478e9c52 100644 --- a/hesabixCore/src/Service/AIService.php +++ b/hesabixCore/src/Service/AIService.php @@ -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) + ]; } } \ No newline at end of file diff --git a/hesabixCore/src/Service/Access.php b/hesabixCore/src/Service/Access.php index a93cc04f..8e0a83f0 100644 --- a/hesabixCore/src/Service/Access.php +++ b/hesabixCore/src/Service/Access.php @@ -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]))){ diff --git a/webUI/package.json b/webUI/package.json index 191b73a7..d2930fdd 100644 --- a/webUI/package.json +++ b/webUI/package.json @@ -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", diff --git a/webUI/src/views/acc/App.vue b/webUI/src/views/acc/App.vue index 97c43214..37efb7ce 100644 --- a/webUI/src/views/acc/App.vue +++ b/webUI/src/views/acc/App.vue @@ -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 { {{ getShortcutKey('/acc/inquiry/panel') }} + + + + هوش مصنوعی + {{ getShortcutKey('/acc/wizard/home') }} + +