328 lines
13 KiB
TypeScript
328 lines
13 KiB
TypeScript
/**
|
|
* ToolActivityLog -- Real-time tool call activity display.
|
|
*
|
|
* Renders tool calls in a human-readable format:
|
|
* - Friendly tool names instead of internal identifiers
|
|
* - Args filtered to hide UUIDs/internal codes on success
|
|
* - Full details shown on error for debugging
|
|
*/
|
|
|
|
import React, { useState} from 'react';
|
|
import type { ToolActivity } from './useWorkspace';
|
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
|
|
|
interface ToolActivityLogProps {
|
|
activities: ToolActivity[];
|
|
}
|
|
|
|
const _UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
|
const _HIDDEN_ARG_KEYS = new Set([
|
|
'mandateId', 'userId', 'featureInstanceId', 'workflowId', 'sessionId',
|
|
]);
|
|
|
|
function _isInternalValue(v: unknown): boolean {
|
|
if (typeof v !== 'string') return false;
|
|
if (_UUID_RE.test(v)) return true;
|
|
if (v.length > 60 && !v.includes(' ')) return true;
|
|
return false;
|
|
}
|
|
|
|
function _formatArgs(args: Record<string, any>, isError: boolean): string {
|
|
const parts: string[] = [];
|
|
for (const [k, v] of Object.entries(args)) {
|
|
if (!isError && _HIDDEN_ARG_KEYS.has(k)) continue;
|
|
if (!isError && _isInternalValue(v)) continue;
|
|
|
|
let display: string;
|
|
if (typeof v === 'string') {
|
|
display = v.length > 80 ? v.slice(0, 77) + '...' : v;
|
|
} else if (typeof v === 'number' || typeof v === 'boolean') {
|
|
display = String(v);
|
|
} else if (v === null || v === undefined) {
|
|
continue;
|
|
} else {
|
|
const json = JSON.stringify(v);
|
|
display = json.length > 80 ? json.slice(0, 77) + '...' : json;
|
|
}
|
|
parts.push(`${k}: ${display}`);
|
|
}
|
|
return parts.join(', ') || (isError ? JSON.stringify(args) : '');
|
|
}
|
|
|
|
export const ToolActivityLog: React.FC<ToolActivityLogProps> = ({ activities }) => {
|
|
const { t } = useLanguage();
|
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
|
|
const _formatResult = (result: string): string => {
|
|
if (!result) return '';
|
|
const trimmed = result.trim();
|
|
|
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
try {
|
|
const parsed = JSON.parse(trimmed);
|
|
if (Array.isArray(parsed)) {
|
|
return t('{count} Ergebnisse', { count: parsed.length });
|
|
}
|
|
if (typeof parsed === 'object' && parsed !== null) {
|
|
const keys = Object.keys(parsed);
|
|
if (parsed.count !== undefined) return t('{count} Einträge', { count: parsed.count });
|
|
if (parsed.total !== undefined) return t('{count} Einträge', { count: parsed.total });
|
|
if (parsed.rows && Array.isArray(parsed.rows)) {
|
|
return t('{count} Zeilen', { count: parsed.rows.length });
|
|
}
|
|
if (parsed.data && Array.isArray(parsed.data)) {
|
|
return t('{count} Einträge', { count: parsed.data.length });
|
|
}
|
|
if (parsed.result !== undefined) {
|
|
const r = String(parsed.result);
|
|
return r.length > 120 ? r.slice(0, 117) + '...' : r;
|
|
}
|
|
if (keys.length <= 3) {
|
|
return keys.map((k) => `${k}: ${String(parsed[k]).slice(0, 40)}`).join(', ');
|
|
}
|
|
return t('Objekt ({count} Felder)', { count: keys.length });
|
|
}
|
|
} catch {
|
|
// not valid JSON
|
|
}
|
|
}
|
|
|
|
return trimmed.length > 150 ? trimmed.slice(0, 147) + '...' : trimmed;
|
|
};
|
|
|
|
const _getToolLabel = (toolName: string): string => {
|
|
switch (toolName) {
|
|
case 'addNode': return t('Knoten hinzufügen');
|
|
case 'aggregateTable': return t('Tabelle aggregieren');
|
|
case 'browseContainer': return t('Datei durchsuchen');
|
|
case 'browseDataSource': return t('Datenquelle durchsuchen');
|
|
case 'browseTable': return t('Tabelle durchsuchen');
|
|
case 'clickup_createTask': return t('ClickUp-Aufgabe erstellen');
|
|
case 'clickup_searchTasks': return t('ClickUp-Aufgaben suchen');
|
|
case 'clickup_updateTask': return t('ClickUp-Aufgabe aktualisieren');
|
|
case 'connectNodes': return t('Knoten verbinden');
|
|
case 'copyFile': return t('Datei kopieren');
|
|
case 'createChart': return t('Diagramm erstellen');
|
|
case 'createGroup': return t('Gruppe anlegen');
|
|
case 'createFolder': return t('Ordner anlegen');
|
|
case 'createRecord': return t('Datensatz erstellen');
|
|
case 'deleteFile': return t('Datei löschen');
|
|
case 'deleteGroup': return t('Gruppe löschen');
|
|
case 'deleteFolder': return t('Ordner löschen');
|
|
case 'deleteRecord': return t('Datensatz löschen');
|
|
case 'describeImage': return t('Bild beschreiben');
|
|
case 'detectLanguage': return t('Sprache erkennen');
|
|
case 'downloadFromDataSource': return t('Aus Datenquelle laden');
|
|
case 'executeCode': return t('Code ausführen');
|
|
case 'extractContainerItem': return t('Element extrahieren');
|
|
case 'generateImage': return t('Bild erzeugen');
|
|
case 'getFileInfo': return t('Datei-Info abrufen');
|
|
case 'getTableSchema': return t('Tabellenschema abrufen');
|
|
case 'jira_connect': return t('Jira verbinden');
|
|
case 'jira_exportTickets': return t('Jira-Tickets exportieren');
|
|
case 'jira_importTickets': return t('Jira-Tickets importieren');
|
|
case 'listAvailableNodeTypes': return t('Verfügbare Knotentypen auflisten');
|
|
case 'listConnections': return t('Verbindungen auflisten');
|
|
case 'listFiles': return t('Dateien auflisten');
|
|
case 'listGroups': return t('Gruppen auflisten');
|
|
case 'listItemsInGroup': return t('Gruppeninhalt auflisten');
|
|
case 'addItemsToGroup': return t('Zu Gruppe hinzufügen');
|
|
case 'moveItemsBetweenGroups': return t('Zwischen Gruppen verschieben');
|
|
case 'ensureInstanceGroup': return t('Instanzgruppe sicherstellen');
|
|
case 'ensureTempGroup': return t('Temp-Gruppe sicherstellen');
|
|
case 'listFolders': return t('Ordner auflisten');
|
|
case 'listTables': return t('Tabellen auflisten');
|
|
case 'listWorkflowHistory': return t('Workflow-Verlauf');
|
|
case 'moveFile': return t('Datei verschieben');
|
|
case 'moveGroup': return t('Gruppe verschieben');
|
|
case 'renameGroup': return t('Gruppe umbenennen');
|
|
case 'moveFolder': return t('Ordner verschieben');
|
|
case 'neutralizeData': return t('Daten neutralisieren');
|
|
case 'outlook_composeAndDraftReply': return t('Outlook-Antwort entwerfen');
|
|
case 'outlook_readEmails': return t('Outlook-E-Mails lesen');
|
|
case 'outlook_searchEmails': return t('Outlook durchsuchen');
|
|
case 'outlook_sendDraft': return t('Outlook-Entwurf senden');
|
|
case 'queryFeatureInstance': return t('Feature abfragen');
|
|
case 'queryTable': return t('Tabelle abfragen');
|
|
case 'readContentObjects': return t('Inhalte lesen');
|
|
case 'readFile': return t('Datei lesen');
|
|
case 'readUrl': return t('URL lesen');
|
|
case 'readWorkflowGraph': return t('Workflow-Graph lesen');
|
|
case 'readWorkflowMessages': return t('Workflow-Nachrichten lesen');
|
|
case 'removeNode': return t('Knoten entfernen');
|
|
case 'renderDocument': return t('Dokument rendern');
|
|
case 'replaceInFile': return t('Text in Datei ersetzen');
|
|
case 'requestToolbox': return t('Toolbox anfordern');
|
|
case 'renameFile': return t('Datei umbenennen');
|
|
case 'renameFolder': return t('Ordner umbenennen');
|
|
case 'searchDataSource': return t('In Datenquelle suchen');
|
|
case 'searchDocuments': return t('Dokumente suchen');
|
|
case 'searchInFileContent': return t('In Datei suchen');
|
|
case 'sendMail': return t('E-Mail senden');
|
|
case 'setNodeParameter': return t('Knotenparameter setzen');
|
|
case 'sharepoint_findDocuments': return t('SharePoint-Dokumente finden');
|
|
case 'sharepoint_readDocuments': return t('SharePoint-Dokumente lesen');
|
|
case 'sharepoint_upload': return t('SharePoint-Upload');
|
|
case 'speechToText': return t('Sprache zu Text');
|
|
case 'summarizeContent': return t('Inhalt zusammenfassen');
|
|
case 'tagFile': return t('Datei taggen');
|
|
case 'textToSpeech': return t('Text zu Sprache');
|
|
case 'translateText': return t('Text übersetzen');
|
|
case 'trustee_refreshAccountingData': return t('Treuhand-Buchhaltung aktualisieren');
|
|
case 'uploadToExternal': return t('Extern hochladen');
|
|
case 'validateGraph': return t('Graph validieren');
|
|
case 'webSearch': return t('Websuche');
|
|
case 'writeFile': return t('Datei schreiben');
|
|
default: return toolName;
|
|
}
|
|
};
|
|
|
|
const _getStatusLabel = (status: string): string => {
|
|
switch (status) {
|
|
case 'calling': return t('läuft');
|
|
case 'success': return t('OK');
|
|
case 'error': return t('Fehler');
|
|
default: return status;
|
|
}
|
|
};
|
|
|
|
if (!activities.length) {
|
|
return (
|
|
<div style={{ padding: 16, textAlign: 'center', color: 'var(--color-text-secondary, #999)', fontSize: 12 }}>
|
|
{t('Noch keine Aktivität')}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={{ padding: 8 }}>
|
|
{activities.map(activity => {
|
|
const isError = activity.status === 'error';
|
|
const isExpanded = expandedId === activity.id;
|
|
const friendlyName = _getToolLabel(activity.toolName);
|
|
const argsText = activity.args && Object.keys(activity.args).length > 0
|
|
? _formatArgs(activity.args, isError)
|
|
: '';
|
|
const resultText = activity.result ? _formatResult(activity.result) : '';
|
|
|
|
return (
|
|
<div
|
|
key={activity.id}
|
|
style={{
|
|
padding: '8px 10px',
|
|
marginBottom: 6,
|
|
borderRadius: 6,
|
|
fontSize: 12,
|
|
border: `1px solid ${
|
|
activity.status === 'calling'
|
|
? 'var(--color-warning, #ffc107)'
|
|
: activity.status === 'success'
|
|
? 'var(--color-success, #4caf50)'
|
|
: 'var(--color-error, #f44336)'
|
|
}30`,
|
|
background: activity.status === 'calling'
|
|
? 'rgba(255, 193, 7, 0.08)'
|
|
: activity.status === 'success'
|
|
? 'rgba(76, 175, 80, 0.06)'
|
|
: 'rgba(244, 67, 54, 0.06)',
|
|
cursor: 'pointer',
|
|
transition: 'background 0.15s ease',
|
|
}}
|
|
onClick={() => setExpandedId(isExpanded ? null : activity.id)}
|
|
>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<span style={{ fontWeight: 600 }}>
|
|
{friendlyName}
|
|
{friendlyName !== activity.toolName && (
|
|
<span style={{ fontWeight: 400, opacity: 0.5, marginLeft: 6, fontSize: 10 }}>
|
|
{activity.toolName}
|
|
</span>
|
|
)}
|
|
</span>
|
|
<span style={{
|
|
fontSize: 10,
|
|
padding: '1px 6px',
|
|
borderRadius: 3,
|
|
background: activity.status === 'calling'
|
|
? 'var(--color-warning, #ffc107)'
|
|
: activity.status === 'success'
|
|
? 'var(--color-success, #4caf50)'
|
|
: 'var(--color-error, #f44336)',
|
|
color: '#fff',
|
|
}}>
|
|
{_getStatusLabel(activity.status)}
|
|
</span>
|
|
</div>
|
|
|
|
{argsText && (
|
|
<div style={{ marginTop: 4, color: 'var(--color-text-secondary, #666)', fontSize: 11 }}>
|
|
{argsText}
|
|
</div>
|
|
)}
|
|
|
|
{resultText && !isError && (
|
|
<div style={{
|
|
marginTop: 4,
|
|
color: 'var(--color-success, #388e3c)',
|
|
fontSize: 11,
|
|
maxHeight: isExpanded ? 'none' : 40,
|
|
overflow: 'hidden',
|
|
}}>
|
|
{resultText}
|
|
</div>
|
|
)}
|
|
|
|
{activity.error && (
|
|
<div style={{ marginTop: 4, color: 'var(--color-error, #c62828)', fontSize: 11 }}>
|
|
{activity.error}
|
|
</div>
|
|
)}
|
|
|
|
{isExpanded && activity.args && Object.keys(activity.args).length > 0 && (
|
|
<details open style={{ marginTop: 6 }}>
|
|
<summary style={{ fontSize: 10, color: 'var(--color-text-secondary, #888)', cursor: 'pointer' }}>
|
|
{t('Alle Parameter')}
|
|
</summary>
|
|
<pre style={{
|
|
fontSize: 10,
|
|
margin: '4px 0 0',
|
|
padding: 6,
|
|
background: 'rgba(0,0,0,0.04)',
|
|
borderRadius: 4,
|
|
overflow: 'auto',
|
|
maxHeight: 200,
|
|
whiteSpace: 'pre-wrap',
|
|
wordBreak: 'break-all',
|
|
}}>
|
|
{JSON.stringify(activity.args, null, 2)}
|
|
</pre>
|
|
</details>
|
|
)}
|
|
|
|
{isExpanded && activity.result && (
|
|
<details open style={{ marginTop: 4 }}>
|
|
<summary style={{ fontSize: 10, color: 'var(--color-text-secondary, #888)', cursor: 'pointer' }}>
|
|
{t('Vollständiges Ergebnis')}
|
|
</summary>
|
|
<pre style={{
|
|
fontSize: 10,
|
|
margin: '4px 0 0',
|
|
padding: 6,
|
|
background: 'rgba(0,0,0,0.04)',
|
|
borderRadius: 4,
|
|
overflow: 'auto',
|
|
maxHeight: 300,
|
|
whiteSpace: 'pre-wrap',
|
|
wordBreak: 'break-all',
|
|
}}>
|
|
{activity.result}
|
|
</pre>
|
|
</details>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
};
|