optimize ux think mode
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const chatEl = document.getElementById('chat');
|
const chatEl = document.getElementById('chat');
|
||||||
const promptEl = document.getElementById('prompt');
|
const promptEl = document.getElementById('prompt');
|
||||||
const sendBtn = document.getElementById('send');
|
const sendBtn = document.getElementById('send');
|
||||||
const abortBtn = document.getElementById('abort');
|
const abortBtn = document.getElementById('abort');
|
||||||
const clearBtn = document.getElementById('clear');
|
const clearBtn = document.getElementById('clear');
|
||||||
|
|
||||||
@@ -32,14 +32,165 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return addMessage('assistant', 'AI is thinking…', 'loader');
|
return addMessage('assistant', 'AI is thinking…', 'loader');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanupEmptyBlocks(container) {
|
||||||
|
container.querySelectorAll('p, div, li, blockquote').forEach((el) => {
|
||||||
|
const html = el.innerHTML
|
||||||
|
.replace(/<br\s*\/?>/gi, '')
|
||||||
|
.replace(/ /gi, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
const text = (el.textContent || '').trim();
|
||||||
|
|
||||||
|
if (html === '' || text === '') {
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripAllThinkContent(container) {
|
||||||
|
const blockSelector = 'p, div, li, blockquote';
|
||||||
|
const thinkSpans = Array.from(container.querySelectorAll('.think'));
|
||||||
|
|
||||||
|
if (thinkSpans.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handledBlocks = new Set();
|
||||||
|
|
||||||
|
thinkSpans.forEach((span) => {
|
||||||
|
const block = span.closest(blockSelector) || span.parentElement;
|
||||||
|
|
||||||
|
if (!block || handledBlocks.has(block)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handledBlocks.add(block);
|
||||||
|
|
||||||
|
const thinksInBlock = Array.from(block.querySelectorAll('.think'));
|
||||||
|
const lastThinkInBlock = thinksInBlock[thinksInBlock.length - 1];
|
||||||
|
|
||||||
|
if (!lastThinkInBlock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let node = block.firstChild;
|
||||||
|
|
||||||
|
while (node) {
|
||||||
|
const next = node.nextSibling;
|
||||||
|
node.remove();
|
||||||
|
|
||||||
|
if (node === lastThinkInBlock) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (
|
||||||
|
block.firstChild &&
|
||||||
|
(
|
||||||
|
(block.firstChild.nodeType === Node.TEXT_NODE &&
|
||||||
|
block.firstChild.textContent.trim() === '') ||
|
||||||
|
(block.firstChild.nodeType === Node.ELEMENT_NODE &&
|
||||||
|
block.firstChild.tagName === 'BR')
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
block.firstChild.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cleanupEmptyBlocks(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasNonThinkContent(container) {
|
||||||
|
const clone = container.cloneNode(true);
|
||||||
|
|
||||||
|
stripAllThinkContent(clone);
|
||||||
|
cleanupEmptyBlocks(clone);
|
||||||
|
|
||||||
|
if ((clone.textContent || '').trim() !== '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone.querySelector(
|
||||||
|
'img, table, pre, code, ul, ol, h1, h2, h3, h4, h5, h6, a'
|
||||||
|
) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupThinkSpans(container) {
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thinkSpans = Array.from(container.querySelectorAll('.think'));
|
||||||
|
|
||||||
|
if (thinkSpans.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNonThinkContent(container)) {
|
||||||
|
stripAllThinkContent(container);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thinkSpans.length <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockSelector = 'p, div, li, blockquote';
|
||||||
|
const lastThink = thinkSpans[thinkSpans.length - 1];
|
||||||
|
const lastBlock = lastThink.closest(blockSelector) || lastThink.parentElement;
|
||||||
|
|
||||||
|
thinkSpans.slice(0, -1).forEach((span) => {
|
||||||
|
const block = span.closest(blockSelector) || span.parentElement;
|
||||||
|
|
||||||
|
if (block && block !== lastBlock) {
|
||||||
|
block.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block === lastBlock) {
|
||||||
|
span.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lastBlock && lastBlock.contains(lastThink)) {
|
||||||
|
let node = lastBlock.firstChild;
|
||||||
|
|
||||||
|
while (node && node !== lastThink) {
|
||||||
|
const next = node.nextSibling;
|
||||||
|
node.remove();
|
||||||
|
node = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (
|
||||||
|
lastThink.nextSibling &&
|
||||||
|
(
|
||||||
|
(lastThink.nextSibling.nodeType === Node.TEXT_NODE &&
|
||||||
|
lastThink.nextSibling.textContent.trim() === '') ||
|
||||||
|
(lastThink.nextSibling.nodeType === Node.ELEMENT_NODE &&
|
||||||
|
lastThink.nextSibling.tagName === 'BR')
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
lastThink.nextSibling.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupEmptyBlocks(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBubbleContent(bubble, raw) {
|
||||||
|
bubble.innerHTML = renderMarkdown(raw);
|
||||||
|
cleanupThinkSpans(bubble);
|
||||||
|
chatEl.scrollTop = chatEl.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadHistory() {
|
async function loadHistory() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/history');
|
const res = await fetch('/history');
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const messages = await res.json();
|
const messages = await res.json();
|
||||||
messages.forEach(m =>
|
messages.forEach(m => addMessage(m.role, renderMarkdown(m.text)));
|
||||||
addMessage(m.role, renderMarkdown(m.text))
|
|
||||||
);
|
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +198,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
promptEl.addEventListener('keydown', (e) => {
|
promptEl.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault(); // verhindert normalen Zeilenumbruch
|
e.preventDefault();
|
||||||
sendBtn.click(); // löst vorhandene Send-Logik aus
|
sendBtn.click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,14 +215,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let firstChunk = true;
|
let firstChunk = true;
|
||||||
let renderTimer = null;
|
let renderTimer = null;
|
||||||
|
|
||||||
// 🔥 LÖSUNG: Throttled Rendering - maximal alle 100ms
|
|
||||||
function scheduleRender() {
|
function scheduleRender() {
|
||||||
if (renderTimer) return;
|
if (renderTimer) return;
|
||||||
|
|
||||||
renderTimer = setTimeout(() => {
|
renderTimer = setTimeout(() => {
|
||||||
// Der StreamChunker sendet bereits korrekt strukturierte Chunks
|
renderBubbleContent(bubble, raw);
|
||||||
bubble.innerHTML = renderMarkdown(raw);
|
|
||||||
chatEl.scrollTop = chatEl.scrollHeight;
|
|
||||||
renderTimer = null;
|
renderTimer = null;
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
@@ -101,14 +249,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const chunk = decoder.decode(value, { stream: true });
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
sseBuffer += chunk;
|
sseBuffer += chunk;
|
||||||
|
|
||||||
// Parse SSE Events (können mehrere data:-Zeilen haben)
|
|
||||||
const events = sseBuffer.split('\n\n');
|
const events = sseBuffer.split('\n\n');
|
||||||
sseBuffer = events.pop() || ''; // Letzten unvollständigen Event behalten
|
sseBuffer = events.pop() || '';
|
||||||
|
|
||||||
events.forEach(event => {
|
events.forEach(event => {
|
||||||
if (!event.trim()) return;
|
if (!event.trim()) return;
|
||||||
|
|
||||||
// Sammle alle "data:"-Zeilen und füge \n wieder ein
|
|
||||||
const dataLines = event
|
const dataLines = event
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter(line => line.startsWith('data: '))
|
.filter(line => line.startsWith('data: '))
|
||||||
@@ -116,20 +262,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
if (dataLines.length === 0) return;
|
if (dataLines.length === 0) return;
|
||||||
|
|
||||||
// Verbinde mit \n (so wie es vom Backend kam)
|
|
||||||
const text = dataLines.join('\n');
|
const text = dataLines.join('\n');
|
||||||
|
|
||||||
if (text === '[DONE]') {
|
if (text === '[DONE]') {
|
||||||
|
|
||||||
// 🔥 Final render flush
|
|
||||||
if (renderTimer) {
|
if (renderTimer) {
|
||||||
clearTimeout(renderTimer);
|
clearTimeout(renderTimer);
|
||||||
renderTimer = null;
|
renderTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
bubble.innerHTML = renderMarkdown(raw);
|
renderBubbleContent(bubble, raw);
|
||||||
chatEl.scrollTop = chatEl.scrollHeight;
|
|
||||||
|
|
||||||
abort = true;
|
abort = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -141,16 +282,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
document.getElementById('ai-cloud')?.classList.add('d-none');
|
document.getElementById('ai-cloud')?.classList.add('d-none');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text sammeln und verzögert rendern
|
|
||||||
raw += text;
|
raw += text;
|
||||||
scheduleRender();
|
scheduleRender();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
bubble.innerHTML += '<br><em>Error occurred.</em>';
|
bubble.innerHTML += '<br><em>Error occurred.</em>';
|
||||||
} finally {
|
} finally {
|
||||||
if (renderTimer) clearTimeout(renderTimer);
|
if (renderTimer) {
|
||||||
|
clearTimeout(renderTimer);
|
||||||
|
}
|
||||||
|
|
||||||
sendBtn.disabled = false;
|
sendBtn.disabled = false;
|
||||||
abortBtn.disabled = true;
|
abortBtn.disabled = true;
|
||||||
clearBtn.disabled = false;
|
clearBtn.disabled = false;
|
||||||
|
|||||||
@@ -371,4 +371,42 @@ body {
|
|||||||
}
|
}
|
||||||
.text-glow{
|
.text-glow{
|
||||||
text-shadow: #86b7fe 2px 1px 14px;
|
text-shadow: #86b7fe 2px 1px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.think {
|
||||||
|
color: #86b7fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.think {
|
||||||
|
display: inline-block;
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
background-image: linear-gradient(
|
||||||
|
100deg,
|
||||||
|
rgba(255, 255, 255, 0) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 32%,
|
||||||
|
rgba(255, 255, 255, 1) 50%,
|
||||||
|
rgba(255, 255, 255, 0) 58%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
);
|
||||||
|
background-size: 180% 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 220% 0;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
animation: thinkScan 2.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes thinkScan {
|
||||||
|
0% {
|
||||||
|
background-position: 180% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -20% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.think {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -13,10 +13,11 @@ use App\Knowledge\Retrieval\RetrieverInterface;
|
|||||||
use Generator;
|
use Generator;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
use App\Agent\StreamChunker;
|
|
||||||
|
|
||||||
final readonly class AgentRunner
|
final readonly class AgentRunner
|
||||||
{
|
{
|
||||||
|
private bool $systemMsgOn;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private PromptBuilder $promptBuilder,
|
private PromptBuilder $promptBuilder,
|
||||||
private ThinkSuppressor $thinkSuppressor,
|
private ThinkSuppressor $thinkSuppressor,
|
||||||
@@ -32,12 +33,14 @@ final readonly class AgentRunner
|
|||||||
private bool $logContext,
|
private bool $logContext,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
$this->systemMsgOn = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(string $prompt, string $userId, ?bool $includeFullContext = false): Generator
|
public function run(string $prompt, string $userId, ?bool $includeFullContext = true): Generator
|
||||||
{
|
{
|
||||||
$prompt = trim($prompt);
|
$prompt = trim($prompt);
|
||||||
$shopResults = [];
|
$shopResults = [];
|
||||||
|
$swagFullOutPut = '';
|
||||||
|
|
||||||
if ($prompt === '') {
|
if ($prompt === '') {
|
||||||
yield '❌ Empty prompt.';
|
yield '❌ Empty prompt.';
|
||||||
@@ -53,6 +56,49 @@ final readonly class AgentRunner
|
|||||||
// 1) Context strategy
|
// 1) Context strategy
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
//$includeFullContext = false;
|
//$includeFullContext = false;
|
||||||
|
if ($includeFullContext) {
|
||||||
|
|
||||||
|
yield $this->systemMsg("Ich analyse deine Anfrage...", "think");
|
||||||
|
|
||||||
|
$promptSwagSearch = '
|
||||||
|
Erzeuge aus dem folgenden Nutzereingabetext einen kurzen Suchtext für die Shopware-6-Suche.
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
- Gib nur den finalen Suchtext aus.
|
||||||
|
- Keine Einleitung, keine Erklärung, keine Anführungszeichen.
|
||||||
|
- Verwende nur die wichtigsten Suchbegriffe aus dem Text.
|
||||||
|
- Maximal 6 Keywords, besser weniger.
|
||||||
|
- Entferne Füllwörter, Höflichkeitsformen und irrelevante Wörter.
|
||||||
|
- Erhalte Produktnamen, Marken, Modellnummern und zusammengesetzte Begriffe exakt, wenn sie relevant sind.
|
||||||
|
- Zahlen, die zu einem Produktnamen oder Modell gehören, müssen erhalten bleiben.
|
||||||
|
- Trenne die Begriffe nur durch Leerzeichen.
|
||||||
|
|
||||||
|
Ausgabeformat:
|
||||||
|
Keyword1 Keyword2 Keyword3
|
||||||
|
|
||||||
|
Text: ' . $prompt . '
|
||||||
|
';
|
||||||
|
|
||||||
|
$this->thinkSuppressor->reset();
|
||||||
|
foreach ($this->ollamaClient->stream($promptSwagSearch) as $swagToken) {
|
||||||
|
|
||||||
|
if (!is_string($swagToken)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$swagCleanToken = $this->thinkSuppressor->filter($swagToken);
|
||||||
|
|
||||||
|
if ($swagCleanToken === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$swagFullOutPut .= $swagCleanToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield $this->systemMsg("Ich habe folgende Keywords an die Shopsuche geschickt: " . $swagFullOutPut, "think");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
// 2) Extract URL content (if present)
|
// 2) Extract URL content (if present)
|
||||||
@@ -62,7 +108,7 @@ final readonly class AgentRunner
|
|||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
// 3) Retrieve RAG knowledge
|
// 3) Retrieve RAG knowledge
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
yield "Hole Daten aus dem RAG Wissen... \n";
|
yield $this->systemMsg("Ich hole relevante Daten aus meinem RAG Wissen...", "think");
|
||||||
|
|
||||||
$knowledgeChunks = $this->retriever->retrieve($prompt);
|
$knowledgeChunks = $this->retriever->retrieve($prompt);
|
||||||
|
|
||||||
@@ -71,11 +117,11 @@ final readonly class AgentRunner
|
|||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
$commerceMeta = $this->commerceIntentLite->detect($prompt);
|
$commerceMeta = $this->commerceIntentLite->detect($prompt);
|
||||||
$commerceIntent = (string) ($commerceMeta['intent'] ?? CommerceIntentLite::NONE);
|
$commerceIntent = (string)($commerceMeta['intent'] ?? CommerceIntentLite::NONE);
|
||||||
|
|
||||||
if($commerceIntent === CommerceIntentLite::ADVISORY_PRODUCT_SEARCH || $commerceIntent === CommerceIntentLite::PRODUCT_SEARCH){
|
if ($commerceIntent === CommerceIntentLite::ADVISORY_PRODUCT_SEARCH || $commerceIntent === CommerceIntentLite::PRODUCT_SEARCH) {
|
||||||
yield "Rufe Shop auf (type: ".$commerceIntent.")... \n";
|
yield $this->systemMsg("Rufe Shop auf (type: " . $commerceIntent . ")", "think");
|
||||||
$shopResults = $this->shopSearchService->search($prompt,$commerceIntent);
|
$shopResults = $swagFullOutPut ? $this->shopSearchService->search($swagFullOutPut, $commerceIntent) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($commerceIntent === CommerceIntentLite::PRODUCT_SEARCH) {
|
if ($commerceIntent === CommerceIntentLite::PRODUCT_SEARCH) {
|
||||||
@@ -84,13 +130,13 @@ final readonly class AgentRunner
|
|||||||
$knowledgeChunks = array_slice($knowledgeChunks, 0, 3);
|
$knowledgeChunks = array_slice($knowledgeChunks, 0, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
if($shopResults){
|
if ($shopResults) {
|
||||||
yield "Verarbeite Shopdaten... \n<hr>\n";
|
yield $this->systemMsg("Verarbeite Shopdaten...", "think");
|
||||||
}else{
|
} else {
|
||||||
yield "Keine releveanten Shopdaten gefunden... \n<hr>\n";
|
yield $this->systemMsg("Keine releveanten Shopdaten gefunden...", "think");
|
||||||
}
|
}
|
||||||
|
|
||||||
yield "Denke nach...\n<hr>\n";
|
yield $this->systemMsg("Denke nach...", "think");
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
@@ -123,6 +169,8 @@ final readonly class AgentRunner
|
|||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
$fullOutput = '';
|
$fullOutput = '';
|
||||||
$chunker = new StreamChunker();
|
$chunker = new StreamChunker();
|
||||||
|
$chunker->flush();
|
||||||
|
$this->thinkSuppressor->reset();
|
||||||
|
|
||||||
foreach ($this->ollamaClient->stream($finalPrompt) as $token) {
|
foreach ($this->ollamaClient->stream($finalPrompt) as $token) {
|
||||||
|
|
||||||
@@ -142,16 +190,16 @@ final readonly class AgentRunner
|
|||||||
// ⬇️ Token in Chunker geben
|
// ⬇️ Token in Chunker geben
|
||||||
$chunk = $chunker->push($cleanToken);
|
$chunk = $chunker->push($cleanToken);
|
||||||
if ($chunk !== null) {
|
if ($chunk !== null) {
|
||||||
yield $chunk;
|
yield $this->systemMsg($chunk, 'answer');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⬇️ Rest flushen
|
// ⬇️ Rest flushen
|
||||||
$finalChunk = $chunker->flush();
|
$finalChunk = $chunker->flush();
|
||||||
if ($finalChunk !== null) {
|
if ($finalChunk !== null) {
|
||||||
yield $finalChunk;
|
yield $this->systemMsg($finalChunk, 'answer');
|
||||||
} else {
|
} else {
|
||||||
yield '... no data received from llm';
|
yield $this->systemMsg('... no data received from llm', 'err');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
@@ -176,7 +224,20 @@ final readonly class AgentRunner
|
|||||||
'exception' => $e,
|
'exception' => $e,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
yield "\n❌ An internal error occurred while processing the request. \nError: " . $e->getMessage();
|
$this->systemMsg("\n❌ An internal error occurred while processing the request. \nError: " . $e->getMessage(), 'err');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function systemMsg(string $msg, string $type = ''): string
|
||||||
|
{
|
||||||
|
if (!$this->systemMsgOn) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($type) {
|
||||||
|
'answer' => '' . $msg,
|
||||||
|
'err' => '<span class="text-danger">' . $msg . "</span>\n<hr>\n",
|
||||||
|
'think' => '<span class="text-info think">' . $msg . "</span>\n"
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user