Fixes
This commit is contained in:
parent
5a9dfe8d5a
commit
285e0e2d33
@ -46,13 +46,13 @@ services:
|
||||
networks:
|
||||
- network
|
||||
restart: always
|
||||
# deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: 1 # alternatively, use `count: all` for all GPUs
|
||||
# capabilities: [gpu]
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1 # alternatively, use `count: all` for all GPUs
|
||||
capabilities: [gpu]
|
||||
webhook:
|
||||
image: tarampampam/webhook-tester:2
|
||||
restart: always
|
||||
|
||||
@ -18,7 +18,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
*/
|
||||
final class ModelClient implements ModelClientInterface
|
||||
{
|
||||
private readonly EventSourceHttpClient $httpClient;
|
||||
private readonly HttpClientInterface $httpClient;
|
||||
|
||||
public function __construct(
|
||||
HttpClientInterface $httpClient,
|
||||
@ -44,13 +44,23 @@ final class ModelClient implements ModelClientInterface
|
||||
*/
|
||||
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) {
|
||||
$headers['Authorization'] = 'Bearer ' . $this->apiKey;
|
||||
}
|
||||
|
||||
$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}}
|
||||
if (!empty($options['tools'])) {
|
||||
$body['tools'] = array_map(
|
||||
@ -77,7 +87,7 @@ final class ModelClient implements ModelClientInterface
|
||||
$response = $this->httpClient->request('POST', rtrim($this->baseUrl, '/') . $this->path, [
|
||||
'headers' => $headers,
|
||||
'json' => $body,
|
||||
'timeout' => 120,
|
||||
'timeout' => 180,
|
||||
]);
|
||||
|
||||
return new RawHttpResult($response);
|
||||
|
||||
@ -4,33 +4,47 @@ namespace PSC\Component\ApiBundle\Api\Shop;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Nelmio\ApiDocBundle\Attribute\Model;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Nelmio\ApiDocBundle\Attribute\Security;
|
||||
use OpenApi\Attributes\JsonContent;
|
||||
use OpenApi\Attributes\Response;
|
||||
use OpenApi\Attributes\Tag;
|
||||
use PSC\Component\ApiBundle\Dto\Error\NotFound;
|
||||
use PSC\Component\ApiBundle\Dto\Shop\Shops;
|
||||
use PSC\Component\ApiBundle\Model\Shop;
|
||||
use PSC\Component\ApiBundle\Model\Shop\Domain;
|
||||
use PSC\Shop\EntityBundle\Repository\ShopContactRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
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\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Get extends AbstractController
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private \PSC\Component\ApiBundle\Hydrate\Shop $hydrateShop;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager,
|
||||
\PSC\Component\ApiBundle\Hydrate\Shop $hydrateShop,
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->hydrateShop = $hydrateShop;
|
||||
private EntityManagerInterface $entityManager,
|
||||
private \PSC\Component\ApiBundle\Hydrate\Shop $hydrateShop,
|
||||
private ShopContactRepository $shopContactRepository,
|
||||
) {}
|
||||
|
||||
#[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(
|
||||
@ -45,7 +59,7 @@ class Get extends AbstractController
|
||||
#[IsGranted('ROLE_SHOP')]
|
||||
public function allAction(): JsonResponse
|
||||
{
|
||||
$shops = $this->entityManager->getRepository(\PSC\Shop\EntityBundle\Entity\Shop::class)->findAll();
|
||||
$shops = $this->shopContactRepository->myEditableShops($this->getUser());
|
||||
|
||||
$data = [];
|
||||
/** @var \PSC\Shop\EntityBundle\Entity\Shop $shop */
|
||||
|
||||
@ -13,9 +13,11 @@
|
||||
|
||||
namespace PSC\Shop\EntityBundle\Repository;
|
||||
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use PSC\Shop\EntityBundle\Entity\Contact;
|
||||
use PSC\Shop\EntityBundle\Entity\ShopContact;
|
||||
|
||||
/**
|
||||
* ShopContactRepository
|
||||
@ -26,8 +28,13 @@ use PSC\Shop\EntityBundle\Entity\Contact;
|
||||
* @package PSC\Shop\Entity
|
||||
* @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
|
||||
*
|
||||
@ -42,12 +49,13 @@ class ShopContactRepository extends EntityRepository
|
||||
|
||||
public function myEditableShops(Contact $contact)
|
||||
{
|
||||
$query = $this->getEntityManager()->createQuery(
|
||||
'
|
||||
$query = $this
|
||||
->getEntityManager()
|
||||
->createQuery('
|
||||
SELECT sc FROM PSC\Shop\EntityBundle\Entity\ShopContact sc
|
||||
JOIN sc.shop s
|
||||
WHERE sc.contact = :id AND sc.admin = 1 ORDER BY sc.admin ASC, s.title ASC',
|
||||
)->setParameter('id', $contact->getId());
|
||||
WHERE sc.contact = :id AND sc.admin = 1 ORDER BY sc.admin ASC, s.title ASC')
|
||||
->setParameter('id', $contact->getId());
|
||||
try {
|
||||
$result = $query->execute();
|
||||
$tmpCollection = new ArrayCollection();
|
||||
@ -62,12 +70,15 @@ class ShopContactRepository extends EntityRepository
|
||||
|
||||
public function myEditableShopsWithNoDeleted(Contact $contact)
|
||||
{
|
||||
$query = $this->getEntityManager()->createQuery(
|
||||
$query = $this
|
||||
->getEntityManager()
|
||||
->createQuery(
|
||||
'
|
||||
SELECT sc FROM PSC\Shop\EntityBundle\Entity\ShopContact sc
|
||||
JOIN sc.shop s
|
||||
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 {
|
||||
$result = $query->execute();
|
||||
$tmpCollection = new ArrayCollection();
|
||||
@ -82,14 +93,16 @@ class ShopContactRepository extends EntityRepository
|
||||
|
||||
public function changeSelectedShop(Contact $contact, $shop_uuid)
|
||||
{
|
||||
$this->getEntityManager()
|
||||
$this
|
||||
->getEntityManager()
|
||||
->createQuery('
|
||||
UPDATE PSC\Shop\EntityBundle\Entity\ShopContact sc
|
||||
SET sc.selected = 0
|
||||
WHERE sc.contact = :id AND sc.selected = 1')
|
||||
->setParameter('id', $contact->getId())
|
||||
->execute();
|
||||
$this->getEntityManager()
|
||||
$this
|
||||
->getEntityManager()
|
||||
->createQuery('
|
||||
UPDATE PSC\Shop\EntityBundle\Entity\ShopContact sc
|
||||
SET sc.selected = 1
|
||||
@ -101,7 +114,8 @@ class ShopContactRepository extends EntityRepository
|
||||
|
||||
public function updateAdmin($contactId, $shopId, $admin = true)
|
||||
{
|
||||
$this->createQueryBuilder('sc')
|
||||
$this
|
||||
->createQueryBuilder('sc')
|
||||
->update()
|
||||
->set('sc.admin', '?1')
|
||||
->setParameter(1, $admin)
|
||||
@ -114,7 +128,8 @@ class ShopContactRepository extends EntityRepository
|
||||
|
||||
public function resetAdmin($contactId, $admin = true)
|
||||
{
|
||||
$this->createQueryBuilder('sc')
|
||||
$this
|
||||
->createQueryBuilder('sc')
|
||||
->update()
|
||||
->set('sc.admin', '?1')
|
||||
->setParameter(1, $admin)
|
||||
|
||||
@ -452,7 +452,7 @@
|
||||
<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>
|
||||
<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>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>
|
||||
@ -486,12 +486,12 @@
|
||||
<script>
|
||||
const aiModels = {
|
||||
'anthropic': 'claude-sonnet-4-6',
|
||||
'openai': 'gpt-4o',
|
||||
'openai': 'gpt-4o-mini',
|
||||
'google': 'gemini-2.0-flash',
|
||||
'mistral': 'mistral-large-latest',
|
||||
'azure': 'gpt-4o',
|
||||
'bedrock': 'anthropic.claude-3-5-sonnet-20241022-v2:0',
|
||||
'ollama': 'llama3.2',
|
||||
'ollama': 'qwen2.5-coder:3b',
|
||||
};
|
||||
|
||||
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 FormelPanel from './app/FormelPanel.vue'
|
||||
import ParameterPanel from './app/ParameterPanel.vue'
|
||||
import AiPanel from './app/AiPanel.vue'
|
||||
import SaveLayoutDialog from './app/dialogs/SaveLayoutDialog.vue'
|
||||
import LoadLayoutDialog from './app/dialogs/LoadLayoutDialog.vue'
|
||||
import Preview from './app/preview/Preview.vue'
|
||||
@ -38,6 +39,7 @@ function onOuterCollapse() {
|
||||
isOuterOpen.value = false
|
||||
globalStore.setShowFormel(false)
|
||||
globalStore.setShowParameter(false)
|
||||
globalStore.setShowAi(false)
|
||||
}
|
||||
|
||||
function onOuterExpand() {
|
||||
@ -47,7 +49,7 @@ function onOuterExpand() {
|
||||
function toggleFormel() {
|
||||
if (globalStore.showFormel) {
|
||||
globalStore.setShowFormel(false)
|
||||
if (!globalStore.showParameter) {
|
||||
if (!globalStore.showParameter && !globalStore.showAi) {
|
||||
outerCodePanelRef.value?.collapse()
|
||||
}
|
||||
} else {
|
||||
@ -61,7 +63,7 @@ function toggleFormel() {
|
||||
function toggleParameter() {
|
||||
if (globalStore.showParameter) {
|
||||
globalStore.setShowParameter(false)
|
||||
if (!globalStore.showFormel) {
|
||||
if (!globalStore.showFormel && !globalStore.showAi) {
|
||||
outerCodePanelRef.value?.collapse()
|
||||
}
|
||||
} 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>
|
||||
|
||||
<template>
|
||||
@ -124,6 +140,14 @@ function toggleParameter() {
|
||||
>
|
||||
{{ $t('parameter_view') }}
|
||||
</Button>
|
||||
<Button
|
||||
@click="toggleAi"
|
||||
:variant="globalStore.showAi ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ $t('ki_view') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -176,7 +200,7 @@ function toggleParameter() {
|
||||
<!-- Inner panels via v-if — no nested collapsible needed -->
|
||||
<div class="flex-1 min-h-0">
|
||||
<ResizablePanelGroup
|
||||
v-if="globalStore.showFormel || globalStore.showParameter"
|
||||
v-if="globalStore.showFormel || globalStore.showParameter || globalStore.showAi"
|
||||
id="code-panel-group"
|
||||
direction="vertical"
|
||||
class="h-full"
|
||||
@ -184,25 +208,39 @@ function toggleParameter() {
|
||||
<ResizablePanel
|
||||
v-if="globalStore.showFormel"
|
||||
:order="1"
|
||||
:default-size="globalStore.showParameter ? 50 : 100"
|
||||
:default-size="(globalStore.showParameter || globalStore.showAi) ? 50 : 100"
|
||||
:min-size="15"
|
||||
>
|
||||
<FormelPanel />
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle
|
||||
v-if="globalStore.showFormel && globalStore.showParameter"
|
||||
v-if="globalStore.showFormel && (globalStore.showParameter || globalStore.showAi)"
|
||||
with-handle
|
||||
/>
|
||||
|
||||
<ResizablePanel
|
||||
v-if="globalStore.showParameter"
|
||||
:order="2"
|
||||
:default-size="globalStore.showFormel ? 50 : 100"
|
||||
:default-size="(globalStore.showFormel || globalStore.showAi) ? 50 : 100"
|
||||
:min-size="15"
|
||||
>
|
||||
<ParameterPanel />
|
||||
</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>
|
||||
</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',
|
||||
};
|
||||
|
||||
watch(xmlString, (xml) => {
|
||||
store.xml = xml
|
||||
let updatingFromStore = false
|
||||
|
||||
watch(xmlString, (newVal) => {
|
||||
if (updatingFromStore) return
|
||||
store.xml = newVal
|
||||
})
|
||||
|
||||
watch(() => store.xml, (newVal) => {
|
||||
updatingFromStore = true
|
||||
xmlString.value = xmlFormat(newVal)
|
||||
updatingFromStore = false
|
||||
})
|
||||
|
||||
function manualSync() {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import SelectElement from '../../../model/SelectElement';
|
||||
import { computed } from 'vue';
|
||||
import { Input } from '../../../components/ui/input'
|
||||
import { Button } from '../../../components/ui/button'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -10,6 +11,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '../../../components/ui/select'
|
||||
import { useGlobalStore } from '../../../stores/Global'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: SelectElement
|
||||
@ -17,11 +19,12 @@ const props = defineProps<{
|
||||
|
||||
let emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
const theModel = computed<SelectElement>({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value),
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -52,4 +55,7 @@ const theModel = computed<SelectElement>({
|
||||
</Select>
|
||||
<label>{{ $t('container') }}</label>
|
||||
<Input v-model="theModel!.container"/>
|
||||
<Button class="mt-2" @click="globalStore.setShowOptions(true)">
|
||||
{{ $t('edit_options') }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
@ -3,7 +3,6 @@ import { useGlobalStore } from '../../../stores/Global'
|
||||
import SelectElement from '../../../model/SelectElement';
|
||||
import Option from '../../../model/select/Option';
|
||||
import { computed } from 'vue';
|
||||
import { Button } from '../../../components/ui/button'
|
||||
import OptionElement from './OptionElement.vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import {
|
||||
@ -14,7 +13,6 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '../../../components/ui/dialog'
|
||||
|
||||
const props = defineProps<{
|
||||
@ -51,9 +49,6 @@ watch(openModal, (newOpenModal: boolean) => {
|
||||
|
||||
<template>
|
||||
<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]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ $t('edit_options') }}</DialogTitle>
|
||||
|
||||
@ -60,5 +60,12 @@
|
||||
"load": "Laden",
|
||||
"enter_layout_name_alert": "Bitte gib einen Namen für das Layout ein.",
|
||||
"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",
|
||||
"enter_layout_name_alert": "Please enter a name for the 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) => {
|
||||
try {
|
||||
const response = await api.get(`api/media/${uuid}`);
|
||||
|
||||
@ -20,6 +20,8 @@ export const useGlobalStore = defineStore('global', {
|
||||
showLoadLayoutDialog: false,
|
||||
showFormel: false,
|
||||
showParameter: false,
|
||||
showAi: false,
|
||||
aiSnapshots: [] as { xml: string; formulas: string; parameter: string }[],
|
||||
sourceDragUuid: "",
|
||||
dragMode: "",
|
||||
json: "",
|
||||
@ -104,6 +106,45 @@ export const useGlobalStore = defineStore('global', {
|
||||
setShowParameter(value: boolean) {
|
||||
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() {
|
||||
this.syncing = true
|
||||
saveFomulasAndParameterToApi(this.shopUuid, this.formulas, this.parameter).then((result: any) => {
|
||||
|
||||
@ -33,7 +33,7 @@ export default defineConfig({
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
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 $y = 0.0;
|
||||
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
|
||||
{
|
||||
@ -66,12 +66,12 @@ class OcrMarker
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getText(): string
|
||||
public function getText(): ?string
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
public function setText(string $text): self
|
||||
public function setText(?string $text): self
|
||||
{
|
||||
$this->text = $text;
|
||||
return $this;
|
||||
|
||||
@ -13,7 +13,7 @@ class ExportPrintJob extends Base
|
||||
private \PSC\Shop\OrderBundle\Model\Order\Position $position;
|
||||
private \PSC\Shop\OrderBundle\Model\Order $order;
|
||||
|
||||
private string $domain = "";
|
||||
private string $domain = '';
|
||||
|
||||
public function setDomain(string $domain): void
|
||||
{
|
||||
@ -23,21 +23,16 @@ class ExportPrintJob extends Base
|
||||
public function call()
|
||||
{
|
||||
$domain = $this->liveUrl;
|
||||
if($this->test) {
|
||||
if ($this->test) {
|
||||
$domain = $this->stagingUrl;
|
||||
}
|
||||
|
||||
$this->token();
|
||||
$response = $this->client->request(
|
||||
'POST', $domain . 'orders', [
|
||||
'headers' =>
|
||||
[...$this->buildHeaders(), ...$this->buildBearerTokenHeader()]
|
||||
,
|
||||
"json" => $this->buildData(),
|
||||
]
|
||||
);
|
||||
$response = $this->client->request('POST', $domain . 'orders', [
|
||||
'headers' => [...$this->buildHeaders(), ...$this->buildBearerTokenHeader()],
|
||||
'json' => $this->buildData(),
|
||||
]);
|
||||
return $response->toArray();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,60 +52,93 @@ class ExportPrintJob extends Base
|
||||
$auflage = null;
|
||||
$productId = null;
|
||||
|
||||
foreach($infos as $info) {
|
||||
if($info['name'] == 'auflage') {
|
||||
foreach ($infos as $info) {
|
||||
if ($info['name'] == 'auflage') {
|
||||
$auflage = $info['value'];
|
||||
}elseif($info['name'] == 'shipping') {
|
||||
} elseif ($info['name'] == 'shipping') {
|
||||
$shipping = $info['value'];
|
||||
}elseif($info['name'] == 'productId') {
|
||||
} elseif ($info['name'] == 'productId') {
|
||||
$productId = $info['value'];
|
||||
}else {
|
||||
} else {
|
||||
$temp[] = ['name' => $info['name'], 'value' => $info['value']];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @var \TP_Basket_Item $objPosition
|
||||
*/
|
||||
$objPosition = unserialize(($this->position->getData()));
|
||||
*/
|
||||
$objPosition = unserialize($this->position->getData());
|
||||
|
||||
$files = [];
|
||||
if(count($this->position->getUploads()) > 0) {
|
||||
if (count($this->position->getUploads()) > 0) {
|
||||
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() != "") {
|
||||
$files = ['url' => 'https://' . $this->domain . $this->position->getProduct()->getUploadProvidedFile()->getUrl()];
|
||||
if (
|
||||
$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)) {
|
||||
$files = ['url' => 'https://' . $this->domain . '/apps/market/steplayouter/basket/' . $this->order->getUid() . '/' . $this->position->getPos() . '/'.$this->order->getAlias() . '_' . $this->position->getPos().'.pdf'];
|
||||
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',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'invoiceAddress' => [
|
||||
'company' => $this->order->getSenderAddress()->getCompany(),
|
||||
'firstName' => $this->order->getSenderAddress()->getFirstname()?: '',
|
||||
'lastName' => $this->order->getSenderAddress()->getLastname()?: '',
|
||||
'street1' => $this->order->getSenderAddress()->getStreet() . ' ' . $this->order->getSenderAddress()->getHouseNumber(),
|
||||
'firstName' => $this->order->getSenderAddress()->getFirstname() ?: '',
|
||||
'lastName' => $this->order->getSenderAddress()->getLastname() ?: '',
|
||||
'street1' =>
|
||||
$this->order->getSenderAddress()->getStreet()
|
||||
. ' '
|
||||
. $this->order->getSenderAddress()->getHouseNumber(),
|
||||
'postalCode' => $this->order->getSenderAddress()->getZip(),
|
||||
'city' => $this->order->getSenderAddress()->getCity(),
|
||||
'phone' => (string)$this->order->getSenderAddress()->getPhone()?: '0000000',
|
||||
'phone' => (string) $this->order->getSenderAddress()->getPhone() ?: '0000000',
|
||||
'country' => 'DE',
|
||||
],
|
||||
'deliveryAddress' => [
|
||||
'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(),
|
||||
'lastName' => $this->order->getDeliveryAddress()->getLastname(),
|
||||
'firstName' => $this->order->getDeliveryAddress()->getFirstname(),
|
||||
'city' => $this->order->getDeliveryAddress()->getCity(),
|
||||
'phone' => $this->order->getDeliveryAddress()->getPhone()?: '0000000',
|
||||
'phone' => $this->order->getDeliveryAddress()->getPhone() ?: '0000000',
|
||||
'country' => 'DE',
|
||||
],
|
||||
'items' => [[
|
||||
@ -122,9 +150,9 @@ class ExportPrintJob extends Base
|
||||
'deliveryOption' => $shipping,
|
||||
'files' => $files,
|
||||
'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 (
|
||||
<>
|
||||
<div className='flex'>
|
||||
<div className='flex-1'>
|
||||
<div className='flex gap-6'>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<Form schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
onChange={(e) => changeCalc(e.formData)}
|
||||
validator={validator}/>
|
||||
</div>
|
||||
<div className=''>
|
||||
<>
|
||||
<h5>Netto: <Currency price={ price.allNet} /></h5>
|
||||
<h5>Mwert: <Currency price={ price.allVat} /></h5>
|
||||
<h4>Brutto: <Currency price={ price.allGross} /></h4>
|
||||
<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 className='text-sm text-gray-600 mb-1'>Mwst: <Currency price={ price.allVat} /></h5>
|
||||
<h4 className='text-base font-semibold mb-4'>Brutto: <Currency price={ price.allGross} /></h4>
|
||||
<Button onClick={addProduct} type={3} variant={"success"} />
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ class ShopService {
|
||||
|
||||
async getShops(): Promise<Shop[]> {
|
||||
|
||||
return await axios.get('/apps/api/shops', {
|
||||
return await axios.get('/apps/api/my_shops', {
|
||||
headers: {
|
||||
'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