This commit is contained in:
seb
2026-10-08 12:49:31 +02:00
parent 32325eb4aa
commit 3658d76c43
5 changed files with 293 additions and 8 deletions

View File

@@ -24,6 +24,7 @@ import {
SUPPORTED_CATEGORIES,
formatFileSize,
} from './chatAttachments';
import { stripUserEnv } from './userEnv';
const KNOWN_FILE_EXTENSIONS = new Set([
'html', 'htm', 'csv', 'tsv', 'pdf', 'txt', 'text', 'md', 'markdown', 'log',
@@ -199,20 +200,28 @@ export function parseAttachmentTag(attrString) {
};
}
export { stripUserEnv };
/**
* Extract all `[ATTACHMENT ...]` markers and return clean text plus attachment metadata.
* Also strips <USERENV> tags so route context is never rendered in chat views.
* @param {string} rawText
* @returns {{ attachments: Array<{ name: string, path: string, type: string, url: string }>, cleanText: string }}
*/
export function parseAttachmentsFromText(rawText) {
if (typeof rawText !== 'string' || !rawText.includes('[ATTACHMENT')) {
return { attachments: [], cleanText: rawText || '' };
if (typeof rawText !== 'string') {
return { attachments: [], cleanText: '' };
}
const textWithoutUserEnv = stripUserEnv(rawText);
if (!textWithoutUserEnv.includes('[ATTACHMENT')) {
return { attachments: [], cleanText: textWithoutUserEnv };
}
const attachments = [];
const tagRegex = /\[ATTACHMENT\s+([^\]]+)\]/g;
let match;
while ((match = tagRegex.exec(rawText)) !== null) {
while ((match = tagRegex.exec(textWithoutUserEnv)) !== null) {
const parsed = parseAttachmentTag(match[1]);
if (parsed.name || parsed.path) {
attachments.push({
@@ -222,7 +231,7 @@ export function parseAttachmentsFromText(rawText) {
}
}
const cleanText = rawText.replace(tagRegex, '').trim();
const cleanText = textWithoutUserEnv.replace(tagRegex, '').trim();
return { attachments, cleanText };
}

View File

@@ -25,8 +25,9 @@ import ManageSearchIcon from '@mui/icons-material/ManageSearch';
import LensBlurIcon from '@mui/icons-material/LensBlur';
import CalculateIcon from '@mui/icons-material/Calculate';
import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner';
import DirectionsIcon from '@mui/icons-material/Directions';
import HealingMarkdown from './HealingMarkdown';
import { VfsAttachmentLink, parseAttachmentsFromText } from './ChatAttachmentItems';
import { VfsAttachmentLink, parseAttachmentsFromText, stripUserEnv } from './ChatAttachmentItems';
import {
CHROMA,
SHADOW_BUBBLE_ASSISTANT,
@@ -86,6 +87,8 @@ const TOOL_ICON_MAP = {
edit_image: LensBlurIcon,
net_gross_calc: CalculateIcon,
ean_checksum: QrCodeScannerIcon,
'set-user-route': DirectionsIcon,
set_user_route: DirectionsIcon,
};
function formatMessageTime(isoString) {
@@ -875,7 +878,8 @@ function MessageRow({ msg, showTime = true }) {
} catch { }
}
const { attachments: vfsAttachments, cleanText } = parseAttachmentsFromText(textContent);
const { attachments: vfsAttachments, cleanText: rawCleanText } = parseAttachmentsFromText(textContent);
const cleanText = stripUserEnv(rawCleanText);
if (msg.role === 'system') {
return (

View File

@@ -0,0 +1,147 @@
/**
* Resolves the active frontend route (path + search query).
* Falls back to window.location if router is not directly accessible.
* @returns {string} e.g. "/artikel" or "/artikel/ART-10042" or "/artikel?target=kabel"
*/
export function getCurrentRoute() {
if (typeof window !== 'undefined' && window.location) {
const { pathname = '/', search = '' } = window.location;
return `${pathname}${search}`;
}
return '/artikel';
}
/**
* Builds the <USERENV><ROUTE>...</ROUTE></USERENV> XML prefix for LLM agent prompts.
* @param {string} [route]
* @returns {string}
*/
export function buildUserEnvPrefix(route) {
const resolvedRoute = route || getCurrentRoute();
return `<USERENV><ROUTE>${resolvedRoute}</ROUTE></USERENV>`;
}
export const USER_ENV_REGEX = /<USERENV>[\s\S]*?(?:<\/USERENV>|$)\s*/gi;
/**
* Strips <USERENV>...</USERENV> tags from a message so it is not rendered in the chat view.
* @param {string} text
* @returns {string}
*/
export function stripUserEnv(text) {
if (typeof text !== 'string' || !text.includes('<USERENV')) {
return text || '';
}
return text.replace(USER_ENV_REGEX, '').trim();
}
/**
* Prepends the <USERENV><ROUTE>...</ROUTE></USERENV> block to the user prompt if not already present.
* @param {string} text
* @param {string} [route]
* @returns {string}
*/
export function attachUserEnv(text = '', route) {
const trimmed = typeof text === 'string' ? text.trim() : '';
if (trimmed.includes('<USERENV')) {
return trimmed;
}
const prefix = buildUserEnvPrefix(route);
return trimmed ? `${prefix}\n\n${trimmed}` : prefix;
}
/**
* Checks if a tool name is the set-user-route tool.
* @param {string} name
* @returns {boolean}
*/
export function isSetUserRouteTool(name) {
if (!name || typeof name !== 'string') return false;
const n = name.trim().toLowerCase();
return n === 'set-user-route' || n === 'set_user_route';
}
/**
* Extracts the target route string from tool data arguments/parameters/result/content.
* @param {object} data
* @returns {string|null}
*/
export function extractRouteFromToolData(data) {
if (!data) return null;
// 1. From result
if (data.result) {
if (typeof data.result === 'object' && data.result.route) {
return String(data.result.route);
}
if (typeof data.result === 'string') {
try {
const parsed = JSON.parse(data.result);
if (parsed?.route) return String(parsed.route);
} catch {}
}
}
// 2. From arguments or parameters
const args = data.arguments || data.parameters;
if (args) {
if (typeof args === 'object' && args.route) {
return String(args.route);
}
if (typeof args === 'string') {
try {
const parsed = JSON.parse(args);
if (parsed?.route) return String(parsed.route);
} catch {}
}
}
// 3. From content
if (data.content) {
if (typeof data.content === 'object' && data.content.route) {
return String(data.content.route);
}
if (typeof data.content === 'string') {
try {
const parsed = JSON.parse(data.content);
if (parsed?.route) return String(parsed.route);
} catch {}
}
}
return null;
}
const navigatedToolCallIds = new Set();
/**
* Navigates the application to the given client route.
* @param {string} route
* @param {string} [toolCallId]
*/
export async function navigateToClientRoute(route, toolCallId) {
if (!route || typeof route !== 'string') return;
const target = route.trim();
if (!target) return;
if (toolCallId) {
if (navigatedToolCallIds.has(toolCallId)) return;
navigatedToolCallIds.add(toolCallId);
}
try {
const { router } = await import('../../appRouter');
if (router?.navigate) {
const current = router.state?.location
? `${router.state.location.pathname}${router.state.location.search || ''}`
: (typeof window !== 'undefined' ? `${window.location.pathname}${window.location.search || ''}` : '');
if (current !== target) {
void router.navigate(target);
}
return;
}
} catch (err) {
console.warn('[userEnv] Router navigation failed:', err);
}
if (typeof window !== 'undefined' && window.location) {
window.location.href = target;
}
}