Fixes
This commit is contained in:
parent
5a9dfe8d5a
commit
285e0e2d33
@ -46,13 +46,13 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- network
|
- network
|
||||||
restart: always
|
restart: always
|
||||||
# deploy:
|
deploy:
|
||||||
# resources:
|
resources:
|
||||||
# reservations:
|
reservations:
|
||||||
# devices:
|
devices:
|
||||||
# - driver: nvidia
|
- driver: nvidia
|
||||||
# count: 1 # alternatively, use `count: all` for all GPUs
|
count: 1 # alternatively, use `count: all` for all GPUs
|
||||||
# capabilities: [gpu]
|
capabilities: [gpu]
|
||||||
webhook:
|
webhook:
|
||||||
image: tarampampam/webhook-tester:2
|
image: tarampampam/webhook-tester:2
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
@ -18,7 +18,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|||||||
*/
|
*/
|
||||||
final class ModelClient implements ModelClientInterface
|
final class ModelClient implements ModelClientInterface
|
||||||
{
|
{
|
||||||
private readonly EventSourceHttpClient $httpClient;
|
private readonly HttpClientInterface $httpClient;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
HttpClientInterface $httpClient,
|
HttpClientInterface $httpClient,
|
||||||
@ -44,13 +44,23 @@ final class ModelClient implements ModelClientInterface
|
|||||||
*/
|
*/
|
||||||
public function request(Model $model, array|string $payload, array $options = []): RawResultInterface
|
public function request(Model $model, array|string $payload, array $options = []): RawResultInterface
|
||||||
{
|
{
|
||||||
$headers = ['Content-Type' => 'application/json'];
|
$headers = [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
// Lowercase 'accept' ist wichtig: EventSourceHttpClient prüft exakt $options['headers']['accept']
|
||||||
|
// mit ??= — uppercase 'Accept' würde NICHT matchen und trotzdem text/event-stream setzen.
|
||||||
|
'accept' => 'application/json',
|
||||||
|
];
|
||||||
if ($this->apiKey) {
|
if ($this->apiKey) {
|
||||||
$headers['Authorization'] = 'Bearer ' . $this->apiKey;
|
$headers['Authorization'] = 'Bearer ' . $this->apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
$body = array_merge($payload, ['model' => $model->getName()]);
|
$body = array_merge($payload, ['model' => $model->getName()]);
|
||||||
|
|
||||||
|
// Ollama-spezifische Optionen (z.B. num_ctx) direkt in den Body mergen
|
||||||
|
if (!empty($options['ollama_options'])) {
|
||||||
|
$body['options'] = $options['ollama_options'];
|
||||||
|
}
|
||||||
|
|
||||||
// Tools in OpenAI-Format bringen: {type: "function", function: {name, description, parameters}}
|
// Tools in OpenAI-Format bringen: {type: "function", function: {name, description, parameters}}
|
||||||
if (!empty($options['tools'])) {
|
if (!empty($options['tools'])) {
|
||||||
$body['tools'] = array_map(
|
$body['tools'] = array_map(
|
||||||
@ -77,7 +87,7 @@ final class ModelClient implements ModelClientInterface
|
|||||||
$response = $this->httpClient->request('POST', rtrim($this->baseUrl, '/') . $this->path, [
|
$response = $this->httpClient->request('POST', rtrim($this->baseUrl, '/') . $this->path, [
|
||||||
'headers' => $headers,
|
'headers' => $headers,
|
||||||
'json' => $body,
|
'json' => $body,
|
||||||
'timeout' => 120,
|
'timeout' => 180,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return new RawHttpResult($response);
|
return new RawHttpResult($response);
|
||||||
|
|||||||
@ -4,33 +4,47 @@ namespace PSC\Component\ApiBundle\Api\Shop;
|
|||||||
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Nelmio\ApiDocBundle\Attribute\Model;
|
use Nelmio\ApiDocBundle\Attribute\Model;
|
||||||
use OpenApi\Attributes as OA;
|
use Nelmio\ApiDocBundle\Attribute\Security;
|
||||||
use OpenApi\Attributes\JsonContent;
|
use OpenApi\Attributes\JsonContent;
|
||||||
use OpenApi\Attributes\Response;
|
use OpenApi\Attributes\Response;
|
||||||
use OpenApi\Attributes\Tag;
|
use OpenApi\Attributes\Tag;
|
||||||
use PSC\Component\ApiBundle\Dto\Error\NotFound;
|
use PSC\Component\ApiBundle\Dto\Error\NotFound;
|
||||||
use PSC\Component\ApiBundle\Dto\Shop\Shops;
|
use PSC\Component\ApiBundle\Dto\Shop\Shops;
|
||||||
use PSC\Component\ApiBundle\Model\Shop;
|
use PSC\Shop\EntityBundle\Repository\ShopContactRepository;
|
||||||
use PSC\Component\ApiBundle\Model\Shop\Domain;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Response as HttpResponse;
|
|
||||||
use Symfony\Component\HttpKernel\KernelInterface;
|
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
|
||||||
|
|
||||||
class Get extends AbstractController
|
class Get extends AbstractController
|
||||||
{
|
{
|
||||||
private EntityManagerInterface $entityManager;
|
|
||||||
private \PSC\Component\ApiBundle\Hydrate\Shop $hydrateShop;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
\PSC\Component\ApiBundle\Hydrate\Shop $hydrateShop,
|
private \PSC\Component\ApiBundle\Hydrate\Shop $hydrateShop,
|
||||||
) {
|
private ShopContactRepository $shopContactRepository,
|
||||||
$this->entityManager = $entityManager;
|
) {}
|
||||||
$this->hydrateShop = $hydrateShop;
|
|
||||||
|
#[Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'my shops',
|
||||||
|
content: new JsonContent(ref: new Model(type: PSC\Component\ApiBundle\Dto\Shop\Shops\Output::class)),
|
||||||
|
)]
|
||||||
|
#[Tag(name: 'Shops')]
|
||||||
|
#[Security(name: 'ApiKeyAuth')]
|
||||||
|
#[Security(name: 'Bearer')]
|
||||||
|
#[Route(path: '/my_shops', methods: ['GET'])]
|
||||||
|
#[IsGranted('ROLE_SHOP')]
|
||||||
|
public function myShops(): JsonResponse
|
||||||
|
{
|
||||||
|
$shops = $this->entityManager->getRepository(\PSC\Shop\EntityBundle\Entity\Shop::class)->findAll();
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
/** @var \PSC\Shop\EntityBundle\Entity\Shop $shop */
|
||||||
|
foreach ($shops as $shop) {
|
||||||
|
$data[] = $this->hydrateShop->hydrateToModel($shop);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json(new Shops\Output($data));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Response(
|
#[Response(
|
||||||
@ -45,7 +59,7 @@ class Get extends AbstractController
|
|||||||
#[IsGranted('ROLE_SHOP')]
|
#[IsGranted('ROLE_SHOP')]
|
||||||
public function allAction(): JsonResponse
|
public function allAction(): JsonResponse
|
||||||
{
|
{
|
||||||
$shops = $this->entityManager->getRepository(\PSC\Shop\EntityBundle\Entity\Shop::class)->findAll();
|
$shops = $this->shopContactRepository->myEditableShops($this->getUser());
|
||||||
|
|
||||||
$data = [];
|
$data = [];
|
||||||
/** @var \PSC\Shop\EntityBundle\Entity\Shop $shop */
|
/** @var \PSC\Shop\EntityBundle\Entity\Shop $shop */
|
||||||
|
|||||||
@ -13,9 +13,11 @@
|
|||||||
|
|
||||||
namespace PSC\Shop\EntityBundle\Repository;
|
namespace PSC\Shop\EntityBundle\Repository;
|
||||||
|
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
use PSC\Shop\EntityBundle\Entity\Contact;
|
use PSC\Shop\EntityBundle\Entity\Contact;
|
||||||
|
use PSC\Shop\EntityBundle\Entity\ShopContact;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ShopContactRepository
|
* ShopContactRepository
|
||||||
@ -26,8 +28,13 @@ use PSC\Shop\EntityBundle\Entity\Contact;
|
|||||||
* @package PSC\Shop\Entity
|
* @package PSC\Shop\Entity
|
||||||
* @subpackage Repositorys
|
* @subpackage Repositorys
|
||||||
*/
|
*/
|
||||||
class ShopContactRepository extends EntityRepository
|
class ShopContactRepository extends ServiceEntityRepository
|
||||||
{
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, ShopContact::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gibt einen Shop per Apikey zurück
|
* Gibt einen Shop per Apikey zurück
|
||||||
*
|
*
|
||||||
@ -42,12 +49,13 @@ class ShopContactRepository extends EntityRepository
|
|||||||
|
|
||||||
public function myEditableShops(Contact $contact)
|
public function myEditableShops(Contact $contact)
|
||||||
{
|
{
|
||||||
$query = $this->getEntityManager()->createQuery(
|
$query = $this
|
||||||
'
|
->getEntityManager()
|
||||||
|
->createQuery('
|
||||||
SELECT sc FROM PSC\Shop\EntityBundle\Entity\ShopContact sc
|
SELECT sc FROM PSC\Shop\EntityBundle\Entity\ShopContact sc
|
||||||
JOIN sc.shop s
|
JOIN sc.shop s
|
||||||
WHERE sc.contact = :id AND sc.admin = 1 ORDER BY sc.admin ASC, s.title ASC',
|
WHERE sc.contact = :id AND sc.admin = 1 ORDER BY sc.admin ASC, s.title ASC')
|
||||||
)->setParameter('id', $contact->getId());
|
->setParameter('id', $contact->getId());
|
||||||
try {
|
try {
|
||||||
$result = $query->execute();
|
$result = $query->execute();
|
||||||
$tmpCollection = new ArrayCollection();
|
$tmpCollection = new ArrayCollection();
|
||||||
@ -62,12 +70,15 @@ class ShopContactRepository extends EntityRepository
|
|||||||
|
|
||||||
public function myEditableShopsWithNoDeleted(Contact $contact)
|
public function myEditableShopsWithNoDeleted(Contact $contact)
|
||||||
{
|
{
|
||||||
$query = $this->getEntityManager()->createQuery(
|
$query = $this
|
||||||
|
->getEntityManager()
|
||||||
|
->createQuery(
|
||||||
'
|
'
|
||||||
SELECT sc FROM PSC\Shop\EntityBundle\Entity\ShopContact sc
|
SELECT sc FROM PSC\Shop\EntityBundle\Entity\ShopContact sc
|
||||||
JOIN sc.shop s
|
JOIN sc.shop s
|
||||||
WHERE sc.contact = :id AND s.deleted = 0 AND sc.admin = 1 ORDER BY sc.admin ASC, s.title ASC',
|
WHERE sc.contact = :id AND s.deleted = 0 AND sc.admin = 1 ORDER BY sc.admin ASC, s.title ASC',
|
||||||
)->setParameter('id', $contact->getId());
|
)
|
||||||
|
->setParameter('id', $contact->getId());
|
||||||
try {
|
try {
|
||||||
$result = $query->execute();
|
$result = $query->execute();
|
||||||
$tmpCollection = new ArrayCollection();
|
$tmpCollection = new ArrayCollection();
|
||||||
@ -82,14 +93,16 @@ class ShopContactRepository extends EntityRepository
|
|||||||
|
|
||||||
public function changeSelectedShop(Contact $contact, $shop_uuid)
|
public function changeSelectedShop(Contact $contact, $shop_uuid)
|
||||||
{
|
{
|
||||||
$this->getEntityManager()
|
$this
|
||||||
|
->getEntityManager()
|
||||||
->createQuery('
|
->createQuery('
|
||||||
UPDATE PSC\Shop\EntityBundle\Entity\ShopContact sc
|
UPDATE PSC\Shop\EntityBundle\Entity\ShopContact sc
|
||||||
SET sc.selected = 0
|
SET sc.selected = 0
|
||||||
WHERE sc.contact = :id AND sc.selected = 1')
|
WHERE sc.contact = :id AND sc.selected = 1')
|
||||||
->setParameter('id', $contact->getId())
|
->setParameter('id', $contact->getId())
|
||||||
->execute();
|
->execute();
|
||||||
$this->getEntityManager()
|
$this
|
||||||
|
->getEntityManager()
|
||||||
->createQuery('
|
->createQuery('
|
||||||
UPDATE PSC\Shop\EntityBundle\Entity\ShopContact sc
|
UPDATE PSC\Shop\EntityBundle\Entity\ShopContact sc
|
||||||
SET sc.selected = 1
|
SET sc.selected = 1
|
||||||
@ -101,7 +114,8 @@ class ShopContactRepository extends EntityRepository
|
|||||||
|
|
||||||
public function updateAdmin($contactId, $shopId, $admin = true)
|
public function updateAdmin($contactId, $shopId, $admin = true)
|
||||||
{
|
{
|
||||||
$this->createQueryBuilder('sc')
|
$this
|
||||||
|
->createQueryBuilder('sc')
|
||||||
->update()
|
->update()
|
||||||
->set('sc.admin', '?1')
|
->set('sc.admin', '?1')
|
||||||
->setParameter(1, $admin)
|
->setParameter(1, $admin)
|
||||||
@ -114,7 +128,8 @@ class ShopContactRepository extends EntityRepository
|
|||||||
|
|
||||||
public function resetAdmin($contactId, $admin = true)
|
public function resetAdmin($contactId, $admin = true)
|
||||||
{
|
{
|
||||||
$this->createQueryBuilder('sc')
|
$this
|
||||||
|
->createQueryBuilder('sc')
|
||||||
->update()
|
->update()
|
||||||
->set('sc.admin', '?1')
|
->set('sc.admin', '?1')
|
||||||
->setParameter(1, $admin)
|
->setParameter(1, $admin)
|
||||||
|
|||||||
@ -452,7 +452,7 @@
|
|||||||
<table class="table table-sm mt-2">
|
<table class="table table-sm mt-2">
|
||||||
<thead><tr><th>Anbieter</th><th>Empfohlenes Modell</th><th>API Key nötig</th><th>Base URL</th></tr></thead>
|
<thead><tr><th>Anbieter</th><th>Empfohlenes Modell</th><th>API Key nötig</th><th>Base URL</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>Ollama</td><td>llama3.2 / mistral</td><td>Nein</td><td>http://localhost:11434</td></tr>
|
<tr><td>Ollama</td><td>qwen2.5-coder:3b / llama3.2 / mistral</td><td>Nein</td><td>http://localhost:11434</td></tr>
|
||||||
<tr><td>Anthropic</td><td>claude-sonnet-4-6</td><td>Ja</td><td>-</td></tr>
|
<tr><td>Anthropic</td><td>claude-sonnet-4-6</td><td>Ja</td><td>-</td></tr>
|
||||||
<tr><td>OpenAI</td><td>gpt-4o</td><td>Ja</td><td>-</td></tr>
|
<tr><td>OpenAI</td><td>gpt-4o</td><td>Ja</td><td>-</td></tr>
|
||||||
<tr><td>Google</td><td>gemini-2.0-flash</td><td>Ja</td><td>-</td></tr>
|
<tr><td>Google</td><td>gemini-2.0-flash</td><td>Ja</td><td>-</td></tr>
|
||||||
@ -486,12 +486,12 @@
|
|||||||
<script>
|
<script>
|
||||||
const aiModels = {
|
const aiModels = {
|
||||||
'anthropic': 'claude-sonnet-4-6',
|
'anthropic': 'claude-sonnet-4-6',
|
||||||
'openai': 'gpt-4o',
|
'openai': 'gpt-4o-mini',
|
||||||
'google': 'gemini-2.0-flash',
|
'google': 'gemini-2.0-flash',
|
||||||
'mistral': 'mistral-large-latest',
|
'mistral': 'mistral-large-latest',
|
||||||
'azure': 'gpt-4o',
|
'azure': 'gpt-4o',
|
||||||
'bedrock': 'anthropic.claude-3-5-sonnet-20241022-v2:0',
|
'bedrock': 'anthropic.claude-3-5-sonnet-20241022-v2:0',
|
||||||
'ollama': 'llama3.2',
|
'ollama': 'qwen2.5-coder:3b',
|
||||||
};
|
};
|
||||||
|
|
||||||
function updateAiFields() {
|
function updateAiFields() {
|
||||||
|
|||||||
40
src/new/var/plugins/Custom/PSC/FormBuilder/Api/Ai/Chat.php
Normal file
40
src/new/var/plugins/Custom/PSC/FormBuilder/Api/Ai/Chat.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Plugin\Custom\PSC\FormBuilder\Api\Ai;
|
||||||
|
|
||||||
|
use Plugin\Custom\PSC\FormBuilder\Service\FormBuilderAiService;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
use OpenApi\Attributes\Tag;
|
||||||
|
use Nelmio\ApiDocBundle\Attribute\Security;
|
||||||
|
|
||||||
|
class Chat extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly FormBuilderAiService $aiService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route(path: '/ai/chat', methods: ['POST'])]
|
||||||
|
#[Tag('FormBuilder')]
|
||||||
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
|
#[Security(name: 'Bearer')]
|
||||||
|
public function chat(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$body = json_decode($request->getContent(), true);
|
||||||
|
|
||||||
|
$message = $body['message'] ?? '';
|
||||||
|
$xml = $body['xml'] ?? '';
|
||||||
|
$formulas = $body['formulas'] ?? '';
|
||||||
|
$parameter = $body['parameter'] ?? '';
|
||||||
|
$paperDb = $body['paperDb'] ?? '';
|
||||||
|
$history = $body['history'] ?? [];
|
||||||
|
|
||||||
|
$result = $this->aiService->chat($message, $xml, $formulas, $parameter, $paperDb, $history);
|
||||||
|
|
||||||
|
return $this->json($result);
|
||||||
|
// result shape: { reply: string, xml?: string, formulas?: string, parameter?: string }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,6 +18,7 @@ import XmlView from './app/XmlView.vue'
|
|||||||
import PaperDBView from './app/PaperDBView.vue'
|
import PaperDBView from './app/PaperDBView.vue'
|
||||||
import FormelPanel from './app/FormelPanel.vue'
|
import FormelPanel from './app/FormelPanel.vue'
|
||||||
import ParameterPanel from './app/ParameterPanel.vue'
|
import ParameterPanel from './app/ParameterPanel.vue'
|
||||||
|
import AiPanel from './app/AiPanel.vue'
|
||||||
import SaveLayoutDialog from './app/dialogs/SaveLayoutDialog.vue'
|
import SaveLayoutDialog from './app/dialogs/SaveLayoutDialog.vue'
|
||||||
import LoadLayoutDialog from './app/dialogs/LoadLayoutDialog.vue'
|
import LoadLayoutDialog from './app/dialogs/LoadLayoutDialog.vue'
|
||||||
import Preview from './app/preview/Preview.vue'
|
import Preview from './app/preview/Preview.vue'
|
||||||
@ -38,6 +39,7 @@ function onOuterCollapse() {
|
|||||||
isOuterOpen.value = false
|
isOuterOpen.value = false
|
||||||
globalStore.setShowFormel(false)
|
globalStore.setShowFormel(false)
|
||||||
globalStore.setShowParameter(false)
|
globalStore.setShowParameter(false)
|
||||||
|
globalStore.setShowAi(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onOuterExpand() {
|
function onOuterExpand() {
|
||||||
@ -47,7 +49,7 @@ function onOuterExpand() {
|
|||||||
function toggleFormel() {
|
function toggleFormel() {
|
||||||
if (globalStore.showFormel) {
|
if (globalStore.showFormel) {
|
||||||
globalStore.setShowFormel(false)
|
globalStore.setShowFormel(false)
|
||||||
if (!globalStore.showParameter) {
|
if (!globalStore.showParameter && !globalStore.showAi) {
|
||||||
outerCodePanelRef.value?.collapse()
|
outerCodePanelRef.value?.collapse()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -61,7 +63,7 @@ function toggleFormel() {
|
|||||||
function toggleParameter() {
|
function toggleParameter() {
|
||||||
if (globalStore.showParameter) {
|
if (globalStore.showParameter) {
|
||||||
globalStore.setShowParameter(false)
|
globalStore.setShowParameter(false)
|
||||||
if (!globalStore.showFormel) {
|
if (!globalStore.showFormel && !globalStore.showAi) {
|
||||||
outerCodePanelRef.value?.collapse()
|
outerCodePanelRef.value?.collapse()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -71,6 +73,20 @@ function toggleParameter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleAi() {
|
||||||
|
if (globalStore.showAi) {
|
||||||
|
globalStore.setShowAi(false)
|
||||||
|
if (!globalStore.showFormel && !globalStore.showParameter) {
|
||||||
|
outerCodePanelRef.value?.collapse()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
globalStore.setShowAi(true)
|
||||||
|
if (!isOuterOpen.value) {
|
||||||
|
outerCodePanelRef.value?.expand()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -124,6 +140,14 @@ function toggleParameter() {
|
|||||||
>
|
>
|
||||||
{{ $t('parameter_view') }}
|
{{ $t('parameter_view') }}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
@click="toggleAi"
|
||||||
|
:variant="globalStore.showAi ? 'default' : 'outline'"
|
||||||
|
size="sm"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
{{ $t('ki_view') }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -176,7 +200,7 @@ function toggleParameter() {
|
|||||||
<!-- Inner panels via v-if — no nested collapsible needed -->
|
<!-- Inner panels via v-if — no nested collapsible needed -->
|
||||||
<div class="flex-1 min-h-0">
|
<div class="flex-1 min-h-0">
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
v-if="globalStore.showFormel || globalStore.showParameter"
|
v-if="globalStore.showFormel || globalStore.showParameter || globalStore.showAi"
|
||||||
id="code-panel-group"
|
id="code-panel-group"
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
@ -184,25 +208,39 @@ function toggleParameter() {
|
|||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
v-if="globalStore.showFormel"
|
v-if="globalStore.showFormel"
|
||||||
:order="1"
|
:order="1"
|
||||||
:default-size="globalStore.showParameter ? 50 : 100"
|
:default-size="(globalStore.showParameter || globalStore.showAi) ? 50 : 100"
|
||||||
:min-size="15"
|
:min-size="15"
|
||||||
>
|
>
|
||||||
<FormelPanel />
|
<FormelPanel />
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
|
||||||
<ResizableHandle
|
<ResizableHandle
|
||||||
v-if="globalStore.showFormel && globalStore.showParameter"
|
v-if="globalStore.showFormel && (globalStore.showParameter || globalStore.showAi)"
|
||||||
with-handle
|
with-handle
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
v-if="globalStore.showParameter"
|
v-if="globalStore.showParameter"
|
||||||
:order="2"
|
:order="2"
|
||||||
:default-size="globalStore.showFormel ? 50 : 100"
|
:default-size="(globalStore.showFormel || globalStore.showAi) ? 50 : 100"
|
||||||
:min-size="15"
|
:min-size="15"
|
||||||
>
|
>
|
||||||
<ParameterPanel />
|
<ParameterPanel />
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
|
||||||
|
<ResizableHandle
|
||||||
|
v-if="globalStore.showParameter && globalStore.showAi"
|
||||||
|
with-handle
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ResizablePanel
|
||||||
|
v-if="globalStore.showAi"
|
||||||
|
:order="3"
|
||||||
|
:default-size="(globalStore.showFormel || globalStore.showParameter) ? 50 : 100"
|
||||||
|
:min-size="15"
|
||||||
|
>
|
||||||
|
<AiPanel />
|
||||||
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,159 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, nextTick, onUnmounted } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { Button } from '../ui/button'
|
||||||
|
import { useGlobalStore } from '../../stores/Global'
|
||||||
|
import { sendAiMessage } from '../../lib/api'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const store = useGlobalStore()
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
role: 'user' | 'assistant'
|
||||||
|
content: string
|
||||||
|
xml?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = ref<Message[]>([])
|
||||||
|
const input = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const elapsedSeconds = ref(0)
|
||||||
|
const messagesEnd = ref<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
let elapsedTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
function startTimer() {
|
||||||
|
elapsedSeconds.value = 0
|
||||||
|
elapsedTimer = setInterval(() => { elapsedSeconds.value++ }, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTimer() {
|
||||||
|
if (elapsedTimer) {
|
||||||
|
clearInterval(elapsedTimer)
|
||||||
|
elapsedTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(stopTimer)
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
nextTick(() => {
|
||||||
|
messagesEnd.value?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function send() {
|
||||||
|
const message = input.value.trim()
|
||||||
|
if (!message || loading.value) return
|
||||||
|
|
||||||
|
history.value.push({ role: 'user', content: message })
|
||||||
|
input.value = ''
|
||||||
|
loading.value = true
|
||||||
|
startTimer()
|
||||||
|
scrollToBottom()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result: any = await sendAiMessage(
|
||||||
|
message,
|
||||||
|
store.xml,
|
||||||
|
store.formulas,
|
||||||
|
store.parameter,
|
||||||
|
store.paperContainer,
|
||||||
|
history.value.slice(0, -1)
|
||||||
|
)
|
||||||
|
|
||||||
|
let replyText = result.reply || ''
|
||||||
|
|
||||||
|
if (result.xml || result.formulas || result.parameter) {
|
||||||
|
await store.applyAiResult(result)
|
||||||
|
if (result.xml) replyText += (replyText ? '\n' : '') + t('ai_applied_xml')
|
||||||
|
if (result.formulas) replyText += (replyText ? '\n' : '') + t('ai_applied_formulas')
|
||||||
|
if (result.parameter) replyText += (replyText ? '\n' : '') + t('ai_applied_parameter')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!replyText) replyText = '✓'
|
||||||
|
history.value.push({ role: 'assistant', content: replyText, xml: result.xml })
|
||||||
|
} catch {
|
||||||
|
history.value.push({ role: 'assistant', content: t('ai_error') })
|
||||||
|
} finally {
|
||||||
|
stopTimer()
|
||||||
|
loading.value = false
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full flex flex-col overflow-hidden">
|
||||||
|
<div class="px-3 py-1.5 border-b border-gray-200 bg-slate-100 shrink-0 flex items-center justify-between">
|
||||||
|
<span class="text-xs font-semibold text-gray-600 uppercase tracking-wide">{{ $t('ki_view') }}</span>
|
||||||
|
<Button
|
||||||
|
v-if="store.aiSnapshots.length > 0"
|
||||||
|
@click="store.undoAiSnapshot()"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="text-xs h-6 px-2"
|
||||||
|
>
|
||||||
|
↩ Rückgängig
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message list -->
|
||||||
|
<div class="flex-1 overflow-y-auto p-3 space-y-2 min-h-0">
|
||||||
|
<div
|
||||||
|
v-for="(msg, i) in history"
|
||||||
|
:key="i"
|
||||||
|
:class="[
|
||||||
|
'max-w-[85%] rounded-lg px-3 py-2 text-sm break-words',
|
||||||
|
msg.role === 'user'
|
||||||
|
? 'ml-auto bg-primary text-primary-foreground'
|
||||||
|
: 'mr-auto bg-slate-100 text-slate-800'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span class="whitespace-pre-wrap">{{ msg.content }}</span>
|
||||||
|
<details v-if="msg.xml" class="mt-2">
|
||||||
|
<summary class="cursor-pointer text-xs text-slate-500 hover:text-slate-700 select-none">XML anzeigen</summary>
|
||||||
|
<pre class="mt-1 text-xs bg-white rounded border border-slate-200 p-2 overflow-x-auto whitespace-pre">{{ msg.xml }}</pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Spinner -->
|
||||||
|
<div v-if="loading" class="mr-auto flex items-center gap-2 bg-slate-100 rounded-lg px-3 py-2">
|
||||||
|
<svg class="animate-spin h-4 w-4 text-slate-500 shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-xs text-slate-500">{{ elapsedSeconds }}s</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref="messagesEnd" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input area -->
|
||||||
|
<div class="shrink-0 border-t border-gray-200 p-2 flex gap-2">
|
||||||
|
<textarea
|
||||||
|
v-model="input"
|
||||||
|
:placeholder="$t('ai_placeholder')"
|
||||||
|
:disabled="loading"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
rows="2"
|
||||||
|
class="flex-1 resize-none rounded-md border border-gray-300 px-2 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
@click="send"
|
||||||
|
:disabled="loading || !input.trim()"
|
||||||
|
size="sm"
|
||||||
|
class="self-end"
|
||||||
|
>
|
||||||
|
{{ $t('ai_send') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -18,8 +18,17 @@ const cmOptions = {
|
|||||||
theme: 'default',
|
theme: 'default',
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(xmlString, (xml) => {
|
let updatingFromStore = false
|
||||||
store.xml = xml
|
|
||||||
|
watch(xmlString, (newVal) => {
|
||||||
|
if (updatingFromStore) return
|
||||||
|
store.xml = newVal
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => store.xml, (newVal) => {
|
||||||
|
updatingFromStore = true
|
||||||
|
xmlString.value = xmlFormat(newVal)
|
||||||
|
updatingFromStore = false
|
||||||
})
|
})
|
||||||
|
|
||||||
function manualSync() {
|
function manualSync() {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import SelectElement from '../../../model/SelectElement';
|
import SelectElement from '../../../model/SelectElement';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { Input } from '../../../components/ui/input'
|
import { Input } from '../../../components/ui/input'
|
||||||
|
import { Button } from '../../../components/ui/button'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@ -10,6 +11,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '../../../components/ui/select'
|
} from '../../../components/ui/select'
|
||||||
|
import { useGlobalStore } from '../../../stores/Global'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: SelectElement
|
modelValue: SelectElement
|
||||||
@ -17,11 +19,12 @@ const props = defineProps<{
|
|||||||
|
|
||||||
let emit = defineEmits(['update:modelValue'])
|
let emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const globalStore = useGlobalStore()
|
||||||
|
|
||||||
const theModel = computed<SelectElement>({
|
const theModel = computed<SelectElement>({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (value) => emit('update:modelValue', value),
|
set: (value) => emit('update:modelValue', value),
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -52,4 +55,7 @@ const theModel = computed<SelectElement>({
|
|||||||
</Select>
|
</Select>
|
||||||
<label>{{ $t('container') }}</label>
|
<label>{{ $t('container') }}</label>
|
||||||
<Input v-model="theModel!.container"/>
|
<Input v-model="theModel!.container"/>
|
||||||
|
<Button class="mt-2" @click="globalStore.setShowOptions(true)">
|
||||||
|
{{ $t('edit_options') }}
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { useGlobalStore } from '../../../stores/Global'
|
|||||||
import SelectElement from '../../../model/SelectElement';
|
import SelectElement from '../../../model/SelectElement';
|
||||||
import Option from '../../../model/select/Option';
|
import Option from '../../../model/select/Option';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { Button } from '../../../components/ui/button'
|
|
||||||
import OptionElement from './OptionElement.vue'
|
import OptionElement from './OptionElement.vue'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import {
|
import {
|
||||||
@ -14,7 +13,6 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from '../../../components/ui/dialog'
|
} from '../../../components/ui/dialog'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -51,9 +49,6 @@ watch(openModal, (newOpenModal: boolean) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog v-if="globalStore.getActiveItem.type === 3" v-model:open="openModal">
|
<Dialog v-if="globalStore.getActiveItem.type === 3" v-model:open="openModal">
|
||||||
<DialogTrigger>
|
|
||||||
<Button class="mt-2">{{ $t('edit_options') }}</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent class="min-w-full grid-rows-[auto_minmax(0,1fr)_auto] p-4 max-h-[90dvh]">
|
<DialogContent class="min-w-full grid-rows-[auto_minmax(0,1fr)_auto] p-4 max-h-[90dvh]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{{ $t('edit_options') }}</DialogTitle>
|
<DialogTitle>{{ $t('edit_options') }}</DialogTitle>
|
||||||
|
|||||||
@ -60,5 +60,12 @@
|
|||||||
"load": "Laden",
|
"load": "Laden",
|
||||||
"enter_layout_name_alert": "Bitte gib einen Namen für das Layout ein.",
|
"enter_layout_name_alert": "Bitte gib einen Namen für das Layout ein.",
|
||||||
"save_layout_failed_alert": "Layout konnte nicht gespeichert werden.",
|
"save_layout_failed_alert": "Layout konnte nicht gespeichert werden.",
|
||||||
"load_layout_confirm": "Möchtest du dieses Layout wirklich laden? Dein aktuelles Design wird überschrieben."
|
"load_layout_confirm": "Möchtest du dieses Layout wirklich laden? Dein aktuelles Design wird überschrieben.",
|
||||||
|
"ki_view": "KI",
|
||||||
|
"ai_send": "Senden",
|
||||||
|
"ai_placeholder": "Schreibe eine Nachricht...",
|
||||||
|
"ai_applied_xml": "XML wurde übernommen.",
|
||||||
|
"ai_applied_formulas": "Formeln wurden übernommen.",
|
||||||
|
"ai_applied_parameter": "Parameter wurden übernommen.",
|
||||||
|
"ai_error": "Fehler beim Senden der Nachricht."
|
||||||
}
|
}
|
||||||
@ -60,5 +60,12 @@
|
|||||||
"load": "Load",
|
"load": "Load",
|
||||||
"enter_layout_name_alert": "Please enter a name for the layout.",
|
"enter_layout_name_alert": "Please enter a name for the layout.",
|
||||||
"save_layout_failed_alert": "Failed to save layout.",
|
"save_layout_failed_alert": "Failed to save layout.",
|
||||||
"load_layout_confirm": "Are you sure you want to load this layout? This will overwrite your current design."
|
"load_layout_confirm": "Are you sure you want to load this layout? This will overwrite your current design.",
|
||||||
|
"ki_view": "AI",
|
||||||
|
"ai_send": "Send",
|
||||||
|
"ai_placeholder": "Write a message...",
|
||||||
|
"ai_applied_xml": "XML has been applied.",
|
||||||
|
"ai_applied_formulas": "Formulas have been applied.",
|
||||||
|
"ai_applied_parameter": "Parameter has been applied.",
|
||||||
|
"ai_error": "Error sending the message."
|
||||||
}
|
}
|
||||||
@ -174,6 +174,22 @@ export const fetchPreview = async (shopUuid: string, json: object[], values?: Re
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sendAiMessage = async (
|
||||||
|
message: string,
|
||||||
|
xml: string,
|
||||||
|
formulas: string,
|
||||||
|
parameter: string,
|
||||||
|
paperDb: string,
|
||||||
|
history: { role: string; content: string }[]
|
||||||
|
) => {
|
||||||
|
const response = await api.post('api/plugin/custom/psc/formbuilder/ai/chat', {
|
||||||
|
json: { message, xml, formulas, parameter, paperDb, history },
|
||||||
|
timeout: 180000,
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
// returns { reply: string, xml?: string, formulas?: string, parameter?: string }
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchMediaUrl = async (uuid: string) => {
|
export const fetchMediaUrl = async (uuid: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`api/media/${uuid}`);
|
const response = await api.get(`api/media/${uuid}`);
|
||||||
|
|||||||
@ -20,6 +20,8 @@ export const useGlobalStore = defineStore('global', {
|
|||||||
showLoadLayoutDialog: false,
|
showLoadLayoutDialog: false,
|
||||||
showFormel: false,
|
showFormel: false,
|
||||||
showParameter: false,
|
showParameter: false,
|
||||||
|
showAi: false,
|
||||||
|
aiSnapshots: [] as { xml: string; formulas: string; parameter: string }[],
|
||||||
sourceDragUuid: "",
|
sourceDragUuid: "",
|
||||||
dragMode: "",
|
dragMode: "",
|
||||||
json: "",
|
json: "",
|
||||||
@ -104,6 +106,45 @@ export const useGlobalStore = defineStore('global', {
|
|||||||
setShowParameter(value: boolean) {
|
setShowParameter(value: boolean) {
|
||||||
this.showParameter = value
|
this.showParameter = value
|
||||||
},
|
},
|
||||||
|
setShowAi(value: boolean) {
|
||||||
|
this.showAi = value
|
||||||
|
},
|
||||||
|
saveAiSnapshot() {
|
||||||
|
this.aiSnapshots.push({ xml: this.xml, formulas: this.formulas, parameter: this.parameter })
|
||||||
|
if (this.aiSnapshots.length > 10) this.aiSnapshots.shift()
|
||||||
|
},
|
||||||
|
undoAiSnapshot() {
|
||||||
|
const snapshot = this.aiSnapshots.pop()
|
||||||
|
if (!snapshot) return false
|
||||||
|
this.xml = snapshot.xml
|
||||||
|
this.formulas = snapshot.formulas
|
||||||
|
this.parameter = snapshot.parameter
|
||||||
|
const itemStore = useItemStore()
|
||||||
|
return saveXmlToApi(this.productUuid, snapshot.xml).then((result: any) => {
|
||||||
|
this.setXML(result.xml)
|
||||||
|
this.setJSON(result.json)
|
||||||
|
this.formulaData = JSON.parse(result.jsonGraph)
|
||||||
|
itemStore.parseJSON(result.json)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async applyAiResult(result: { xml?: string; formulas?: string; parameter?: string }) {
|
||||||
|
this.saveAiSnapshot()
|
||||||
|
if (result.xml) {
|
||||||
|
const itemStore = useItemStore()
|
||||||
|
this.syncing = true
|
||||||
|
try {
|
||||||
|
const syncResult: any = await saveXmlToApi(this.productUuid, result.xml)
|
||||||
|
this.setXML(syncResult.xml)
|
||||||
|
this.setJSON(syncResult.json)
|
||||||
|
this.formulaData = JSON.parse(syncResult.jsonGraph)
|
||||||
|
itemStore.parseJSON(syncResult.json)
|
||||||
|
} finally {
|
||||||
|
this.syncing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.formulas) this.formulas = result.formulas
|
||||||
|
if (result.parameter) this.parameter = result.parameter
|
||||||
|
},
|
||||||
syncFormulasAndParameter() {
|
syncFormulasAndParameter() {
|
||||||
this.syncing = true
|
this.syncing = true
|
||||||
saveFomulasAndParameterToApi(this.shopUuid, this.formulas, this.parameter).then((result: any) => {
|
saveFomulasAndParameterToApi(this.shopUuid, this.formulas, this.parameter).then((result: any) => {
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export default defineConfig({
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
configure: (proxy) => {
|
configure: (proxy) => {
|
||||||
proxy.on('proxyReq', (proxyReq) => {
|
proxy.on('proxyReq', (proxyReq) => {
|
||||||
proxyReq.setHeader('Authorization', 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NzI2OTc2MzgsImV4cCI6MTc3MjcwMTIzOCwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfU0hPUF9PUEVSQVRPUiIsIlJPTEVfVVNFUiIsIlJPTEVfVVNFUiIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9FZGl0IiwiUk9MRV9QU0NfQ29sbGVjdF9Db250YWN0X0FkZCIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9EZWxldGUiLCJST0xFX1BTQ19Db2xsZWN0X0NvbnRhY3RfTG9jayIsIlJPTEVfUFNDX1IyX1NlbmRjbG91ZF9TaG93Il0sInVpZCI6MX0.PNZeyLmg6vXNYekPb7cm5oDUKcXVbD1tYUWpMdkkAow_Vs0-1NwAUpg-lLCGvYfOmy9_7MWSEdHoaSRAalr1CC8KFdyeLAF1IjFlNRyytCzHUA5g3ygY8_FWykr122U4pexllsrUcxJVif5ekpuUtkpPY4xn4U9F0DvrrDX1xgf9NLOfVaVwv6RkNtiOR8oqnzQKwkuO79KLGOtodjVPxp320yYF9D8kf2mzqjFQLujD1vc_SqD8Sdal3on51RAYqO677gRwWl92vg9-pzAhiRKlwsXvmOnI3ow3PF9ZsyC2Qv7okuyChNlVC8L4ThhMixry-K0jmIZbO0nintidiQ');
|
proxyReq.setHeader('Authorization', 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NzI4MDIxNDksImV4cCI6MTc3MjgwNTc0OSwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfU0hPUF9PUEVSQVRPUiIsIlJPTEVfVVNFUiIsIlJPTEVfVVNFUiIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9FZGl0IiwiUk9MRV9QU0NfQ29sbGVjdF9Db250YWN0X0FkZCIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9EZWxldGUiLCJST0xFX1BTQ19Db2xsZWN0X0NvbnRhY3RfTG9jayIsIlJPTEVfUFNDX1IyX1NlbmRjbG91ZF9TaG93Il0sInVpZCI6MX0.Sq-flCd9Td-P8dstLYms2x3EXdXV3IdR6fbMD2scLHShyGAkebJTJ2tKEVykIRE90oKOIERGKTE8NwlUjQqAfFSI0_oMMYChP7mkw0tgntFjvVWyjVxcCQCvA5AGqQHLnMXpxfbo--NCtnqUzZzg55HeBm67gTpiZIORziwzyvPxHqy-JfURkP3W1c0X1BmXYiM_zAelQCkqwKNsUQBSr9WAV5ZOjKa0ZbRV7cRvktNb1H4UUTEd8somwu6hTgataySBYgWrrL34Kcrokbt3EmchBA70LajRzgZO80Z0_4Ir6IJJkeBUYQnCTNT-fvZmwV7pQZ_nIZjtNNxKweqmng');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,419 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Plugin\Custom\PSC\FormBuilder\Service;
|
||||||
|
|
||||||
|
use PSC\Component\AiBundle\Platform\PlatformFactory;
|
||||||
|
use PSC\System\SettingsBundle\Service\Instance;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\AI\Agent\Agent;
|
||||||
|
use Symfony\AI\Agent\Input;
|
||||||
|
use Symfony\AI\Agent\InputProcessor\SystemPromptInputProcessor;
|
||||||
|
use Symfony\AI\Agent\InputProcessorInterface;
|
||||||
|
use Symfony\AI\Platform\Message\Message;
|
||||||
|
use Symfony\AI\Platform\Message\MessageBag;
|
||||||
|
use Symfony\AI\Platform\Result\TextResult;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
class FormBuilderAiService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Instance $instanceService,
|
||||||
|
private readonly HttpClientInterface $httpClient,
|
||||||
|
#[Autowire(service: 'monolog.logger.ai')]
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function chat(
|
||||||
|
string $message,
|
||||||
|
string $xml,
|
||||||
|
string $formulas,
|
||||||
|
string $parameter,
|
||||||
|
string $paperDb,
|
||||||
|
array $history = [],
|
||||||
|
): array {
|
||||||
|
$instance = $this->instanceService->getInstance();
|
||||||
|
$provider = $instance->getAiProvider();
|
||||||
|
|
||||||
|
if (!$provider) {
|
||||||
|
$this->logger->warning('FormBuilder AI: Kein KI-Anbieter konfiguriert.');
|
||||||
|
return [
|
||||||
|
'reply' => 'Kein KI-Anbieter konfiguriert. Bitte konfiguriere einen KI-Anbieter in den Shop-Einstellungen.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = $instance->getAiModel() ?: 'llama3.2';
|
||||||
|
|
||||||
|
$this->logger->info('FormBuilder AI: Chat-Anfrage', [
|
||||||
|
'provider' => $provider,
|
||||||
|
'model' => $model,
|
||||||
|
'message' => $message,
|
||||||
|
'history_count' => count($history),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$platform = PlatformFactory::create(
|
||||||
|
baseUrl: $this->resolveBaseUrl($instance),
|
||||||
|
apiKey: $instance->getAiApiKey() ?: null,
|
||||||
|
httpClient: $this->httpClient,
|
||||||
|
logger: $this->logger,
|
||||||
|
);
|
||||||
|
|
||||||
|
$systemPrompt = $this->buildSystemPrompt();
|
||||||
|
$systemPromptProcessor = new SystemPromptInputProcessor($systemPrompt);
|
||||||
|
|
||||||
|
$ollamaOptionsProcessor = null;
|
||||||
|
if ($provider === 'ollama') {
|
||||||
|
$ollamaOptionsProcessor = new class implements InputProcessorInterface {
|
||||||
|
public function processInput(Input $input): void
|
||||||
|
{
|
||||||
|
$options = $input->getOptions();
|
||||||
|
$options['ollama_options']['num_ctx'] = 8192;
|
||||||
|
$input->setOptions($options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$inputProcessors = array_filter([$systemPromptProcessor, $ollamaOptionsProcessor]);
|
||||||
|
|
||||||
|
$agent = new Agent(platform: $platform, model: $model, inputProcessors: array_values($inputProcessors));
|
||||||
|
|
||||||
|
$messages = new MessageBag();
|
||||||
|
|
||||||
|
// Few-Shot-Beispiel 1: Einfaches Einfügen
|
||||||
|
$messages->add(Message::ofUser(
|
||||||
|
"AKTUELLER ZUSTAND:\n\nXML:\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<kalkulation>\n <artikel>\n <name>Visitenkarten</name>\n <option id=\"auflage\" name=\"Auflage\" type=\"Input\" default=\"100\" min=\"50\" max=\"5000\"/>\n </artikel>\n</kalkulation>\n\nFormeln:\n(leer)\n\nParameter:\n(leer)\n\nAUFGABE: Füge ein Inputfeld für die Breite zwischen Auflage und dem Ende ein, min 50, max 400.",
|
||||||
|
));
|
||||||
|
$messages->add(Message::ofAssistant(
|
||||||
|
"```xml\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<kalkulation>\n <artikel>\n <name>Visitenkarten</name>\n <option id=\"auflage\" name=\"Auflage\" type=\"Input\" default=\"100\" min=\"50\" max=\"5000\"/>\n <option id=\"breite\" name=\"Breite\" type=\"Input\" default=\"50\" min=\"50\" max=\"400\"/>\n </artikel>\n</kalkulation>\n```\nIch habe das Inputfeld \"Breite\" nach \"Auflage\" eingefügt.",
|
||||||
|
));
|
||||||
|
// Few-Shot-Beispiel 2: Formel mit Feld-ID aus der XML (nicht erfinden!)
|
||||||
|
$messages->add(Message::ofUser(
|
||||||
|
"AKTUELLER ZUSTAND:\n\nXML:\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<kalkulation>\n <artikel>\n <name>Flyer</name>\n <option id=\"auflage\" name=\"Auflage\" type=\"Input\" default=\"500\" min=\"100\" max=\"10000\"/>\n </artikel>\n</kalkulation>\n\nFormeln:\n(leer)\n\nParameter:\n(leer)\n\nAUFGABE: Füge ein verstecktes Preisfeld hinzu das 0.05 pro Stück berechnet.",
|
||||||
|
));
|
||||||
|
$messages->add(Message::ofAssistant(
|
||||||
|
"```xml\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<kalkulation>\n <artikel>\n <name>Flyer</name>\n <option id=\"auflage\" name=\"Auflage\" type=\"Input\" default=\"500\" min=\"100\" max=\"10000\"/>\n <option id=\"preis\" name=\"Preis\" type=\"Hidden\">\n <auflage>\n <grenze formel=\"\$Vauflage\$V*0.05\">1-</grenze>\n </auflage>\n </option>\n </artikel>\n</kalkulation>\n```\nIch habe das Hidden-Feld \"preis\" hinzugefügt. Die Formel verwendet \$Vauflage\$V, weil das Mengenfeld in dieser XML die ID \"auflage\" hat.",
|
||||||
|
));
|
||||||
|
|
||||||
|
// Few-Shot-Beispiel 3: Mehrdimensionale Kalkulation (Select + Auflage → Hidden)
|
||||||
|
$messages->add(Message::ofUser(
|
||||||
|
"AKTUELLER ZUSTAND:\n\nXML:\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<kalkulation>\n <artikel>\n <name>Druck</name>\n <option id=\"auflage\" name=\"Auflage\" type=\"Input\" default=\"100\" min=\"1\" max=\"10000\"/>\n <option id=\"format\" name=\"Format\" type=\"Select\" default=\"a4\">\n <opt id=\"a4\" name=\"A4\"/>\n <opt id=\"a3\" name=\"A3\"/>\n </option>\n </artikel>\n</kalkulation>\n\nFormeln:\n(leer)\n\nParameter:\n(leer)\n\nAUFGABE: Füge ein verstecktes Preisfeld hinzu. A4 kostet 0.10 pro Stück, A3 kostet 0.18 pro Stück.",
|
||||||
|
));
|
||||||
|
$messages->add(Message::ofAssistant(
|
||||||
|
"```xml\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<kalkulation>\n <artikel>\n <name>Druck</name>\n <option id=\"auflage\" name=\"Auflage\" type=\"Input\" default=\"100\" min=\"1\" max=\"10000\"/>\n <option id=\"format\" name=\"Format\" type=\"Select\" default=\"a4\">\n <opt id=\"a4\" name=\"A4\"/>\n <opt id=\"a3\" name=\"A3\"/>\n </option>\n <option id=\"preis\" name=\"Preis\" type=\"Hidden\">\n <format>\n <grenze formel=\"\$Vauflage\$V*0.10\">a4</grenze>\n <grenze formel=\"\$Vauflage\$V*0.18\">a3</grenze>\n </format>\n </option>\n </artikel>\n</kalkulation>\n```\nIch habe das Hidden-Feld \"preis\" mit einer Abhängigkeit von \"format\" eingefügt. Die Grenzen matchen auf die opt-IDs \"a4\" und \"a3\".",
|
||||||
|
));
|
||||||
|
|
||||||
|
foreach ($history as $h) {
|
||||||
|
if ($h['role'] === 'user') {
|
||||||
|
$messages->add(Message::ofUser($h['content']));
|
||||||
|
} elseif ($h['role'] === 'assistant') {
|
||||||
|
$messages->add(Message::ofAssistant($h['content']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$userMessage = $this->buildUserMessage($message, $xml, $formulas, $parameter, $paperDb);
|
||||||
|
$this->logger->info('FormBuilder AI: UserMessage', ['message' => $userMessage]);
|
||||||
|
$messages->add(Message::ofUser($userMessage));
|
||||||
|
|
||||||
|
$result = $agent->call($messages);
|
||||||
|
$text = $result instanceof TextResult ? $result->getContent() : (string) $result;
|
||||||
|
|
||||||
|
$this->logger->info('FormBuilder AI: Raw-Antwort', ['text' => $text]);
|
||||||
|
|
||||||
|
$parsed = $this->parseResponse($text);
|
||||||
|
|
||||||
|
$this->logger->info('FormBuilder AI: Antwort geparst', [
|
||||||
|
'reply_length' => strlen($parsed['reply']),
|
||||||
|
'has_xml' => isset($parsed['xml']),
|
||||||
|
'has_formulas' => isset($parsed['formulas']),
|
||||||
|
'has_parameter' => isset($parsed['parameter']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseResponse(string $text): array
|
||||||
|
{
|
||||||
|
$result = ['reply' => $text];
|
||||||
|
|
||||||
|
// XML: case-insensitive, also match plain ``` blocks containing <kalkulation>
|
||||||
|
if (preg_match('/```xml\s*([\s\S]*?)\s*```/i', $text, $m)) {
|
||||||
|
$result['xml'] = trim($m[1]);
|
||||||
|
} elseif (preg_match('/```\s*(<\?xml[\s\S]*?<\/kalkulation>)\s*```/i', $text, $m)) {
|
||||||
|
$result['xml'] = trim($m[1]);
|
||||||
|
} elseif (preg_match('/(<\?xml[\s\S]*?<\/kalkulation>)/i', $text, $m)) {
|
||||||
|
// Raw XML without code fence
|
||||||
|
$result['xml'] = trim($m[1]);
|
||||||
|
} elseif (preg_match('/(<kalkulation>[\s\S]*?<\/kalkulation>)/i', $text, $m)) {
|
||||||
|
$result['xml'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/```php\s*\/\/ formulas\s*([\s\S]*?)\s*```/i', $text, $m)) {
|
||||||
|
$result['formulas'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/```php\s*\/\/ parameter\s*([\s\S]*?)\s*```/i', $text, $m)) {
|
||||||
|
$result['parameter'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip all code blocks from the reply text
|
||||||
|
$reply = trim(preg_replace('/```[\s\S]*?```/', '', $text));
|
||||||
|
// Also strip raw XML if it was captured without fences
|
||||||
|
if (isset($result['xml'])) {
|
||||||
|
$reply = trim(str_replace($result['xml'], '', $reply));
|
||||||
|
}
|
||||||
|
$result['reply'] = $reply;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildSystemPrompt(): string
|
||||||
|
{
|
||||||
|
return <<<'PROMPT'
|
||||||
|
Du bist ein spezialisiertes KI-Tool für den PrintshopCreator FormBuilder.
|
||||||
|
Antworte auf Deutsch. Führe Änderungen sofort durch.
|
||||||
|
|
||||||
|
REGELN:
|
||||||
|
- Ändere AUSSCHLIESSLICH was der Nutzer explizit angefragt hat.
|
||||||
|
- Bestehende Felder, IDs und Werte NIEMALS umbenennen oder verändern, außer wenn direkt danach gefragt.
|
||||||
|
- Gib bei XML-Änderungen immer die KOMPLETTE XML zurück.
|
||||||
|
- Alle IDs (`id="..."`) immer lowercase: nur Kleinbuchstaben, Ziffern, Unterstrich. Niemals Großbuchstaben oder Sonderzeichen in IDs!
|
||||||
|
- Lies die aktuelle XML im AKTUELLER ZUSTAND-Abschnitt und verwende NUR dort vorhandene Feld-IDs in Formeln und Abhängigkeiten.
|
||||||
|
|
||||||
|
KONVENTION: Mengenfeld immer `id="auflage"`
|
||||||
|
- Wenn eine Kalkulation ein Anzahl-/Mengen-/Auflagenfeld benötigt, heißt dieses immer `id="auflage"`.
|
||||||
|
- In Formeln und Abhängigkeiten: `$Vauflage$V` und `<auflage>...</auflage>`.
|
||||||
|
- Existiert bereits ein Mengenfeld mit anderem Namen, NICHT umbenennen — diese Regel gilt nur für neu angelegte Felder.
|
||||||
|
|
||||||
|
ABSOLUTES VERBOT:
|
||||||
|
- `<grenze>` darf NIEMALS direkt als Kind von `<option>` stehen. Immer innerhalb eines Abhängigkeits-Elements (z.B. `<auflage>`, `<format>`, `<farbe>`).
|
||||||
|
- Formeln (`formel="..."`) dürfen NUR Variablen (`$V...$V`, `$CV...$CV`, `$PM...$P`, `$F...$F`), Zahlen und mathematische Operatoren enthalten. KEIN XML, KEINE Bedingungen, KEINE if/else-Logik in Formeln.
|
||||||
|
- Komplexe Berechnungen die von mehreren Feldern abhängen → `<grenzen>` mit mehreren Abhängigkeits-Elementen verwenden (siehe ABHÄNGIGKEITEN).
|
||||||
|
|
||||||
|
## FELDTYPEN
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Texteingabe mit Grenzen -->
|
||||||
|
<option id="menge" name="Menge" type="Input" default="100" min="1" max="5000" require="true"/>
|
||||||
|
|
||||||
|
<!-- Auswahl -->
|
||||||
|
<option id="farbe" name="Farbe" type="Select" default="rot">
|
||||||
|
<opt id="rot" name="Rot"/>
|
||||||
|
<opt id="blau" name="Blau"/>
|
||||||
|
</option>
|
||||||
|
|
||||||
|
<!-- Papierdatenbank -->
|
||||||
|
<option id="papier" name="Papier" type="Select" mode="papierdb" container="container_id" default="bdm250"/>
|
||||||
|
|
||||||
|
<!-- Verstecktes Berechnungsfeld -->
|
||||||
|
<option id="calc_preis" name="Preis" type="Hidden">
|
||||||
|
<auflage>
|
||||||
|
<grenze formel="$Vmenge$V*0.15">1-</grenze>
|
||||||
|
</auflage>
|
||||||
|
</option>
|
||||||
|
|
||||||
|
<!-- Überschrift / Text / Textarea -->
|
||||||
|
<option id="h1" name="Abschnitt" type="Headline" variant="2"/>
|
||||||
|
<option id="info" name="" type="Text" default="Hinweistext"/>
|
||||||
|
<option id="notiz" name="Notiz" type="Textarea"/>
|
||||||
|
|
||||||
|
<!-- Layout: Zeile mit Spalten -->
|
||||||
|
<option id="zeile1" name="" type="Row">
|
||||||
|
<column id="col1">
|
||||||
|
<option id="breite" name="Breite mm" type="Input" default="100" min="50" max="700"/>
|
||||||
|
</column>
|
||||||
|
<column id="col2">
|
||||||
|
<option id="hoehe" name="Höhe mm" type="Input" default="100" min="50" max="700"/>
|
||||||
|
</column>
|
||||||
|
</option>
|
||||||
|
|
||||||
|
<!-- Gruppe -->
|
||||||
|
<option id="grp" name="Optionen" type="Fieldset">
|
||||||
|
<option id="f1" name="Feld" type="Input"/>
|
||||||
|
</option>
|
||||||
|
```
|
||||||
|
|
||||||
|
## GRENZEN (Wertebereiche / Preisberechnung)
|
||||||
|
|
||||||
|
KRITISCH — Struktur von <grenze>:
|
||||||
|
- Das `formel`-Attribut enthält die BERECHNUNG (z.B. `$Vauflage$V*0.05`)
|
||||||
|
- Der TAG-INHALT enthält NUR den BEREICH oder WERT (z.B. `1-`, `1-100`, `501-`, `42`, `rot`)
|
||||||
|
- NIEMALS eine Formel in den Tag-Inhalt schreiben!
|
||||||
|
|
||||||
|
FALSCH: `<grenze formel="$Vauflage$V*0.05">$Vauflage$V*3</grenze>`
|
||||||
|
RICHTIG: `<grenze formel="$Vauflage$V*0.05">1-</grenze>`
|
||||||
|
|
||||||
|
Mögliche Bereichsangaben im Tag-Inhalt:
|
||||||
|
- `1-` = ab 1 (unbegrenzt oben)
|
||||||
|
- `1-100` = von 1 bis 100
|
||||||
|
- `101-500` = von 101 bis 500
|
||||||
|
- `501-` = ab 501
|
||||||
|
- `42` = genau der Wert 42
|
||||||
|
- `10,20,50` = genau diese Werte
|
||||||
|
- `rot` = der Select-Optionswert "rot"
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Mengenstaffel mit Formel -->
|
||||||
|
<abcfeld>
|
||||||
|
<grenze formel="$Vabcfeld$V*1.20">1-100</grenze>
|
||||||
|
<grenze formel="$Vabcfeld$V*1.10">101-500</grenze>
|
||||||
|
<grenze formel="$Vabcfeld$V*0.95">501-</grenze>
|
||||||
|
</abcfeld>
|
||||||
|
|
||||||
|
<!-- Mit Preis und Pauschale -->
|
||||||
|
<grenze formel="$Vabcfeld$V*0.90" preis="5.00" pauschale="12.50">1-</grenze>
|
||||||
|
|
||||||
|
<!-- Statischer Wert -->
|
||||||
|
<grenze calc_value="(20)">1-</grenze>
|
||||||
|
|
||||||
|
<!-- Mehrere calc_values -->
|
||||||
|
<grenze calc_value="$PMmaschinenkosten$P" calc_value_2="$PMstundenlohn$P">1-</grenze>
|
||||||
|
```
|
||||||
|
|
||||||
|
## ABHÄNGIGKEITEN (Dependencies)
|
||||||
|
|
||||||
|
KRITISCH: Der Name des Abhängigkeits-Elements MUSS exakt die `id` eines vorhandenen Options-Feldes aus der aktuellen XML sein.
|
||||||
|
Schaue in der XML nach: `<option id="XYZ" ...>` → dann heißt das Element `<XYZ>`.
|
||||||
|
NIEMALS einen selbst erfundenen Namen verwenden!
|
||||||
|
|
||||||
|
Beispiel: XML enthält `<option id="auflage" ...>` und `<option id="farbe" ...>`:
|
||||||
|
- Abhängigkeit von Auflage → `<auflage>...</auflage>`
|
||||||
|
- Abhängigkeit von Farbe → `<farbe>...</farbe>`
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Hidden-Feld abhängig vom Select mit id="farbe" -->
|
||||||
|
<option id="aufpreis" name="Aufpreis" type="Hidden">
|
||||||
|
<farbe>
|
||||||
|
<grenze calc_value="0.00">rot</grenze>
|
||||||
|
<grenze calc_value="5.00">blau</grenze>
|
||||||
|
<grenze calc_value="8.00">gruen</grenze>
|
||||||
|
</farbe>
|
||||||
|
</option>
|
||||||
|
|
||||||
|
<!-- Hidden-Feld abhängig vom Input mit id="auflage" -->
|
||||||
|
<option id="rabatt" name="Rabatt" type="Hidden">
|
||||||
|
<auflage>
|
||||||
|
<grenze calc_value="0.00">1-99</grenze>
|
||||||
|
<grenze calc_value="0.05">100-499</grenze>
|
||||||
|
<grenze calc_value="0.10">500-</grenze>
|
||||||
|
</auflage>
|
||||||
|
</option>
|
||||||
|
|
||||||
|
<!-- Select-Optionen mit Mengenstaffel (id des Inputs = "stueck") -->
|
||||||
|
<option id="veredelung" name="Veredelung" type="Select" default="ohne">
|
||||||
|
<opt id="ohne" name="Ohne">
|
||||||
|
<stueck>
|
||||||
|
<grenze calc_value="0">1-</grenze>
|
||||||
|
</stueck>
|
||||||
|
</opt>
|
||||||
|
<opt id="glanzlack" name="Glanzlack">
|
||||||
|
<stueck>
|
||||||
|
<grenze formel="$Vstueck$V*0.08">1-500</grenze>
|
||||||
|
<grenze formel="$Vstueck$V*0.06">501-</grenze>
|
||||||
|
</stueck>
|
||||||
|
</opt>
|
||||||
|
</option>
|
||||||
|
|
||||||
|
<!-- Mehrfache Abhängigkeiten (id="format" und id="veredelung" müssen in XML existieren) -->
|
||||||
|
<option id="druck" name="Druckkosten" type="Hidden">
|
||||||
|
<grenzen>
|
||||||
|
<format>
|
||||||
|
<grenze formel="$Vstueck$V*0.10">a4</grenze>
|
||||||
|
<grenze formel="$Vstueck$V*0.18">a3</grenze>
|
||||||
|
</format>
|
||||||
|
<veredelung>
|
||||||
|
<grenze formel="$CVdruck_format$CV*1.2">glanzlack</grenze>
|
||||||
|
<grenze formel="$CVdruck_format$CV">ohne</grenze>
|
||||||
|
</veredelung>
|
||||||
|
</grenzen>
|
||||||
|
</option>
|
||||||
|
```
|
||||||
|
|
||||||
|
## FORMELVARIABLEN
|
||||||
|
|
||||||
|
KRITISCH: Variablen haben immer ein schließendes Suffix — NIEMALS weglassen!
|
||||||
|
- `$Vfeld_id$V` — Eingabewert eines Input/Select-Feldes
|
||||||
|
- `$CVfeld_id$CV` — Berechneter Wert eines Hidden-Feldes
|
||||||
|
- `$PMparameter_name$P` — Maschinenparameter
|
||||||
|
- `$Fformel_name$F` — Externe Formel
|
||||||
|
- `$PPparameter_name$P` — Shop-Parameter
|
||||||
|
|
||||||
|
FALSCH: `$Vmenge` / `$CVpreis` / `$PMsatz`
|
||||||
|
RICHTIG: `$Vmenge$V` / `$CVpreis$CV` / `$PMsatz$P`
|
||||||
|
|
||||||
|
KRITISCH: Verwende in Formeln und Abhängigkeiten NUR Feld-IDs die in der aktuellen XML vorhanden sind!
|
||||||
|
Beispiel: Wenn das Mengenfeld `auflage` heißt, dann `$Vauflage$V` — NICHT `$Vmenge$V`.
|
||||||
|
Schaue immer zuerst in der aktuellen XML nach den vorhandenen Feld-IDs (id="...").
|
||||||
|
|
||||||
|
Hilfsfunktionen in Formeln: `round()`, `tonumber()`, `ceil()`, `floor()`
|
||||||
|
|
||||||
|
## UPLOADS
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<uploads>
|
||||||
|
<upload id="druckdaten" name="Druckdaten" description="Bitte PDF hochladen">
|
||||||
|
<preflight id="Preflight-PDF"/>
|
||||||
|
</upload>
|
||||||
|
</uploads>
|
||||||
|
```
|
||||||
|
|
||||||
|
## PRECALC (Vorkalkulationen)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<precalc>
|
||||||
|
<group name="Standard">
|
||||||
|
<calc name="100 Stück"><menge>100</menge></calc>
|
||||||
|
<calc name="250 Stück"><menge>250</menge></calc>
|
||||||
|
</group>
|
||||||
|
</precalc>
|
||||||
|
```
|
||||||
|
|
||||||
|
## AUSGABE bei XML-Änderung
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<kalkulation>...</kalkulation>
|
||||||
|
```
|
||||||
|
Dann 1 Satz: was wurde wo eingefügt/geändert.
|
||||||
|
PROMPT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildUserMessage(
|
||||||
|
string $message,
|
||||||
|
string $xml,
|
||||||
|
string $formulas,
|
||||||
|
string $parameter,
|
||||||
|
string $paperDb,
|
||||||
|
): string {
|
||||||
|
$xmlSection = $xml ?: '(leer)';
|
||||||
|
$formulasSection = $formulas ?: '(leer)';
|
||||||
|
$parameterSection = $parameter ?: '(leer)';
|
||||||
|
|
||||||
|
return <<<MSG
|
||||||
|
AKTUELLER ZUSTAND:
|
||||||
|
|
||||||
|
XML:
|
||||||
|
{$xmlSection}
|
||||||
|
|
||||||
|
Formeln:
|
||||||
|
{$formulasSection}
|
||||||
|
|
||||||
|
Parameter:
|
||||||
|
{$parameterSection}
|
||||||
|
|
||||||
|
AUFGABE: {$message}
|
||||||
|
MSG;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveBaseUrl($instance): string
|
||||||
|
{
|
||||||
|
$provider = $instance->getAiProvider();
|
||||||
|
$baseUrl = $instance->getAiBaseUrl();
|
||||||
|
|
||||||
|
return match ($provider) {
|
||||||
|
'ollama' => rtrim($baseUrl ?: 'http://localhost:11434', '/'),
|
||||||
|
'openai' => 'https://api.openai.com',
|
||||||
|
'mistral' => 'https://api.mistral.ai',
|
||||||
|
'azure' => rtrim($baseUrl ?: '', '/'),
|
||||||
|
default => rtrim($baseUrl ?: 'http://localhost:11434', '/'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@ class OcrMarker
|
|||||||
private float $x = 0.0;
|
private float $x = 0.0;
|
||||||
private float $y = 0.0;
|
private float $y = 0.0;
|
||||||
private float $size = 2.0; // Diameter in mm for dots, font size for text, size for triangle
|
private float $size = 2.0; // Diameter in mm for dots, font size for text, size for triangle
|
||||||
private string $text = '';
|
private ?string $text = '';
|
||||||
|
|
||||||
public function getType(): string
|
public function getType(): string
|
||||||
{
|
{
|
||||||
@ -66,12 +66,12 @@ class OcrMarker
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getText(): string
|
public function getText(): ?string
|
||||||
{
|
{
|
||||||
return $this->text;
|
return $this->text;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setText(string $text): self
|
public function setText(?string $text): self
|
||||||
{
|
{
|
||||||
$this->text = $text;
|
$this->text = $text;
|
||||||
return $this;
|
return $this;
|
||||||
|
|||||||
@ -13,7 +13,7 @@ class ExportPrintJob extends Base
|
|||||||
private \PSC\Shop\OrderBundle\Model\Order\Position $position;
|
private \PSC\Shop\OrderBundle\Model\Order\Position $position;
|
||||||
private \PSC\Shop\OrderBundle\Model\Order $order;
|
private \PSC\Shop\OrderBundle\Model\Order $order;
|
||||||
|
|
||||||
private string $domain = "";
|
private string $domain = '';
|
||||||
|
|
||||||
public function setDomain(string $domain): void
|
public function setDomain(string $domain): void
|
||||||
{
|
{
|
||||||
@ -23,21 +23,16 @@ class ExportPrintJob extends Base
|
|||||||
public function call()
|
public function call()
|
||||||
{
|
{
|
||||||
$domain = $this->liveUrl;
|
$domain = $this->liveUrl;
|
||||||
if($this->test) {
|
if ($this->test) {
|
||||||
$domain = $this->stagingUrl;
|
$domain = $this->stagingUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->token();
|
$this->token();
|
||||||
$response = $this->client->request(
|
$response = $this->client->request('POST', $domain . 'orders', [
|
||||||
'POST', $domain . 'orders', [
|
'headers' => [...$this->buildHeaders(), ...$this->buildBearerTokenHeader()],
|
||||||
'headers' =>
|
'json' => $this->buildData(),
|
||||||
[...$this->buildHeaders(), ...$this->buildBearerTokenHeader()]
|
]);
|
||||||
,
|
|
||||||
"json" => $this->buildData(),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
return $response->toArray();
|
return $response->toArray();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,60 +52,93 @@ class ExportPrintJob extends Base
|
|||||||
$auflage = null;
|
$auflage = null;
|
||||||
$productId = null;
|
$productId = null;
|
||||||
|
|
||||||
foreach($infos as $info) {
|
foreach ($infos as $info) {
|
||||||
if($info['name'] == 'auflage') {
|
if ($info['name'] == 'auflage') {
|
||||||
$auflage = $info['value'];
|
$auflage = $info['value'];
|
||||||
}elseif($info['name'] == 'shipping') {
|
} elseif ($info['name'] == 'shipping') {
|
||||||
$shipping = $info['value'];
|
$shipping = $info['value'];
|
||||||
}elseif($info['name'] == 'productId') {
|
} elseif ($info['name'] == 'productId') {
|
||||||
$productId = $info['value'];
|
$productId = $info['value'];
|
||||||
}else {
|
} else {
|
||||||
$temp[] = ['name' => $info['name'], 'value' => $info['value']];
|
$temp[] = ['name' => $info['name'], 'value' => $info['value']];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \TP_Basket_Item $objPosition
|
* @var \TP_Basket_Item $objPosition
|
||||||
*/
|
*/
|
||||||
$objPosition = unserialize(($this->position->getData()));
|
$objPosition = unserialize($this->position->getData());
|
||||||
|
|
||||||
$files = [];
|
$files = [];
|
||||||
if(count($this->position->getUploads()) > 0) {
|
if (count($this->position->getUploads()) > 0) {
|
||||||
foreach ($this->position->getUploads() as $upload) {
|
foreach ($this->position->getUploads() as $upload) {
|
||||||
$files = ['url' => 'https://' . $this->domain . '/'.$upload->getPath()];
|
$files = ['url' => 'https://' . $this->domain . '/' . $upload->getPath()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if($this->position->getProduct()->getUploadProvidedFile() && $this->position->getProduct()->getUploadProvidedFile()->getUrl() != "") {
|
if (
|
||||||
$files = ['url' => 'https://' . $this->domain . $this->position->getProduct()->getUploadProvidedFile()->getUrl()];
|
$this->position->getProduct()->getUploadProvidedFile()
|
||||||
|
&& $this->position->getProduct()->getUploadProvidedFile()->getUrl() != ''
|
||||||
|
) {
|
||||||
|
$files = ['url' => 'https://' . $this->domain . $this->position
|
||||||
|
->getProduct()
|
||||||
|
->getUploadProvidedFile()
|
||||||
|
->getUrl()];
|
||||||
}
|
}
|
||||||
|
|
||||||
$outfile = '/data/www/old/market/steplayouter/basket/' . $this->order->getUid() . '/' . $this->position->getPos() . '/'.$this->order->getAlias() . '_' . $this->position->getPos().'.pdf';
|
$outfile =
|
||||||
|
'/data/www/old/market/steplayouter/basket/'
|
||||||
|
. $this->order->getUid()
|
||||||
|
. '/'
|
||||||
|
. $this->position->getPos()
|
||||||
|
. '/'
|
||||||
|
. $this->order->getAlias()
|
||||||
|
. '_'
|
||||||
|
. $this->position->getPos()
|
||||||
|
. '.pdf';
|
||||||
|
|
||||||
if(file_exists($outfile)) {
|
if (file_exists($outfile)) {
|
||||||
$files = ['url' => 'https://' . $this->domain . '/apps/market/steplayouter/basket/' . $this->order->getUid() . '/' . $this->position->getPos() . '/'.$this->order->getAlias() . '_' . $this->position->getPos().'.pdf'];
|
$files = [
|
||||||
|
'url' =>
|
||||||
|
'https://'
|
||||||
|
. $this->domain
|
||||||
|
. '/apps/market/steplayouter/basket/'
|
||||||
|
. $this->order->getUid()
|
||||||
|
. '/'
|
||||||
|
. $this->position->getPos()
|
||||||
|
. '/'
|
||||||
|
. $this->order->getAlias()
|
||||||
|
. '_'
|
||||||
|
. $this->position->getPos()
|
||||||
|
. '.pdf',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'invoiceAddress' => [
|
'invoiceAddress' => [
|
||||||
'company' => $this->order->getSenderAddress()->getCompany(),
|
'company' => $this->order->getSenderAddress()->getCompany(),
|
||||||
'firstName' => $this->order->getSenderAddress()->getFirstname()?: '',
|
'firstName' => $this->order->getSenderAddress()->getFirstname() ?: '',
|
||||||
'lastName' => $this->order->getSenderAddress()->getLastname()?: '',
|
'lastName' => $this->order->getSenderAddress()->getLastname() ?: '',
|
||||||
'street1' => $this->order->getSenderAddress()->getStreet() . ' ' . $this->order->getSenderAddress()->getHouseNumber(),
|
'street1' =>
|
||||||
|
$this->order->getSenderAddress()->getStreet()
|
||||||
|
. ' '
|
||||||
|
. $this->order->getSenderAddress()->getHouseNumber(),
|
||||||
'postalCode' => $this->order->getSenderAddress()->getZip(),
|
'postalCode' => $this->order->getSenderAddress()->getZip(),
|
||||||
'city' => $this->order->getSenderAddress()->getCity(),
|
'city' => $this->order->getSenderAddress()->getCity(),
|
||||||
'phone' => (string)$this->order->getSenderAddress()->getPhone()?: '0000000',
|
'phone' => (string) $this->order->getSenderAddress()->getPhone() ?: '0000000',
|
||||||
'country' => 'DE',
|
'country' => 'DE',
|
||||||
],
|
],
|
||||||
'deliveryAddress' => [
|
'deliveryAddress' => [
|
||||||
'company' => $this->order->getDeliveryAddress()->getCompany(),
|
'company' => $this->order->getDeliveryAddress()->getCompany(),
|
||||||
'street1' => $this->order->getDeliveryAddress()->getStreet() . ' ' . $this->order->getDeliveryAddress()->getHouseNumber(),
|
'street1' =>
|
||||||
|
$this->order->getDeliveryAddress()->getStreet()
|
||||||
|
. ' '
|
||||||
|
. $this->order->getDeliveryAddress()->getHouseNumber(),
|
||||||
'postalCode' => $this->order->getDeliveryAddress()->getZip(),
|
'postalCode' => $this->order->getDeliveryAddress()->getZip(),
|
||||||
'lastName' => $this->order->getDeliveryAddress()->getLastname(),
|
'lastName' => $this->order->getDeliveryAddress()->getLastname(),
|
||||||
'firstName' => $this->order->getDeliveryAddress()->getFirstname(),
|
'firstName' => $this->order->getDeliveryAddress()->getFirstname(),
|
||||||
'city' => $this->order->getDeliveryAddress()->getCity(),
|
'city' => $this->order->getDeliveryAddress()->getCity(),
|
||||||
'phone' => $this->order->getDeliveryAddress()->getPhone()?: '0000000',
|
'phone' => $this->order->getDeliveryAddress()->getPhone() ?: '0000000',
|
||||||
'country' => 'DE',
|
'country' => 'DE',
|
||||||
],
|
],
|
||||||
'items' => [[
|
'items' => [[
|
||||||
@ -122,9 +150,9 @@ class ExportPrintJob extends Base
|
|||||||
'deliveryOption' => $shipping,
|
'deliveryOption' => $shipping,
|
||||||
'files' => $files,
|
'files' => $files,
|
||||||
'reference' => [
|
'reference' => [
|
||||||
'text' => $this->order->getAlias() . ' ' . $this->position->getPos()
|
'text' => $this->order->getAlias() . ' ' . $this->position->getPos(),
|
||||||
]
|
],
|
||||||
]]
|
]],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -89,21 +89,19 @@ const ProductForm = ({shop, pos, handleClose, handleChange}) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='flex'>
|
<div className='flex gap-6'>
|
||||||
<div className='flex-1'>
|
<div className='flex-1 min-w-0'>
|
||||||
<Form schema={schema}
|
<Form schema={schema}
|
||||||
uiSchema={uiSchema}
|
uiSchema={uiSchema}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onChange={(e) => changeCalc(e.formData)}
|
onChange={(e) => changeCalc(e.formData)}
|
||||||
validator={validator}/>
|
validator={validator}/>
|
||||||
</div>
|
</div>
|
||||||
<div className=''>
|
<div className='w-48 shrink-0 border-l border-gray-200 pl-6 pt-2'>
|
||||||
<>
|
<h5 className='text-sm text-gray-600 mb-1'>Netto: <Currency price={ price.allNet} /></h5>
|
||||||
<h5>Netto: <Currency price={ price.allNet} /></h5>
|
<h5 className='text-sm text-gray-600 mb-1'>Mwst: <Currency price={ price.allVat} /></h5>
|
||||||
<h5>Mwert: <Currency price={ price.allVat} /></h5>
|
<h4 className='text-base font-semibold mb-4'>Brutto: <Currency price={ price.allGross} /></h4>
|
||||||
<h4>Brutto: <Currency price={ price.allGross} /></h4>
|
|
||||||
<Button onClick={addProduct} type={3} variant={"success"} />
|
<Button onClick={addProduct} type={3} variant={"success"} />
|
||||||
</>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ class ShopService {
|
|||||||
|
|
||||||
async getShops(): Promise<Shop[]> {
|
async getShops(): Promise<Shop[]> {
|
||||||
|
|
||||||
return await axios.get('/apps/api/shops', {
|
return await axios.get('/apps/api/my_shops', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Bearer ' + this.token.currentToken
|
'Authorization': 'Bearer ' + this.token.currentToken
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user