This commit is contained in:
seb
2026-10-05 23:35:51 +02:00
parent 2daeb7ebbf
commit 4445bc259e

View File

@@ -8,11 +8,10 @@ import PsychologyRoundedIcon from '@mui/icons-material/PsychologyRounded';
import BuildRoundedIcon from '@mui/icons-material/BuildRounded';
import HealingMarkdown from './HealingMarkdown';
import {
CHROMA,
SHADOW_BUBBLE_ASSISTANT,
SHADOW_BUBBLE_USER,
SURFACE_RAISED,
SURFACE_SUBTLE_PAIR,
byMode,
} from '../../colors';
function formatMessageTime(isoString) {
@@ -47,44 +46,42 @@ function formatUsageText(usage) {
}
function formatToolTooltip(content, result, parameters) {
let text = '';
const parts = [];
if (parameters !== undefined && parameters !== null && parameters !== '') {
try {
const parsed = typeof parameters === 'string' ? JSON.parse(parameters) : parameters;
text = 'Aufruf:\n' + (typeof parsed === 'object' ? JSON.stringify(parsed, null, 2) : String(parameters));
parts.push(typeof parsed === 'object' ? JSON.stringify(parsed, null, 2) : String(parameters));
} catch {
text = 'Aufruf:\n' + String(parameters);
parts.push(String(parameters));
}
}
let contentText = '';
if (content !== undefined && content !== null && content !== '') {
try {
const parsed = typeof content === 'string' ? JSON.parse(content) : content;
contentText = typeof parsed === 'object' ? JSON.stringify(parsed, null, 2) : String(content);
const str = typeof parsed === 'object' ? JSON.stringify(parsed, null, 2) : String(content);
if (!parts.includes(str)) {
parts.push(str);
}
} catch {
contentText = String(content);
}
}
if (contentText) {
if (text) {
text += '\n\nErgebnis:\n' + contentText;
} else {
text = contentText;
const str = String(content);
if (!parts.includes(str)) {
parts.push(str);
}
}
}
if (result !== undefined && result !== null) {
try {
const resStr = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result);
if (!text.includes(resStr)) {
text = text ? text + '\n\n' + resStr : resStr;
if (!parts.some((p) => p.includes(resStr))) {
parts.push(resStr);
}
} catch {}
}
return text || 'Werkzeug wird ausgeführt…';
return parts.join('\n\n') || '';
}
class ClickTooltip extends PureComponent {
@@ -110,6 +107,10 @@ class ClickTooltip extends PureComponent {
disableFocusListener
disableHoverListener
disableTouchListener
enterDelay={0}
leaveDelay={0}
TransitionProps={{ timeout: 0 }}
slotProps={{ transition: { timeout: 0 } }}
title={title}
arrow
placement="top"
@@ -138,23 +139,19 @@ function HandshakeIconView({ state }) {
return (
<Box
sx={(theme) => ({
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 24,
height: 24,
color: isGreen
? theme.palette.mode === 'dark'
? '#66bb6a'
: '#2e7d32'
? CHROMA.GREEN.dark
: isBlack
? theme.palette.mode === 'dark'
? '#f0f0f0'
: '#111111'
? '#000000'
: '#9e9e9e',
transition: 'color 180ms ease-in-out',
})}
}}
>
<HandshakeRoundedIcon sx={{ fontSize: 20, color: 'inherit' }} />
</Box>
@@ -163,7 +160,7 @@ function HandshakeIconView({ state }) {
function BrainIconView({ reasoning }) {
const tooltipContent = reasoning?.text?.trim() || '';
if (!tooltipContent) return null;
if (!tooltipContent && reasoning?.sealed) return null;
return (
<ClickTooltip
@@ -179,23 +176,23 @@ function BrainIconView({ reasoning }) {
lineHeight: 1.45,
}}
>
{tooltipContent}
{tooltipContent || 'Wird überlegt…'}
</Box>
}
>
<Box
sx={(theme) => ({
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 24,
height: 24,
color: theme.palette.mode === 'dark' ? '#b388ff' : '#651fff',
color: CHROMA.PURPLE.dark,
borderRadius: 1,
'&:hover': {
bgcolor: 'action.hover',
},
})}
}}
>
<PsychologyRoundedIcon sx={{ fontSize: 20, color: 'inherit' }} />
</Box>
@@ -227,24 +224,18 @@ function WorkIconView({ tool }) {
}
>
<Box
sx={(theme) => ({
sx={{
display: 'inline-flex',
alignItems: 'center',
height: 24,
gap: 0.5,
color: isGreen
? theme.palette.mode === 'dark'
? '#66bb6a'
: '#2e7d32'
: theme.palette.mode === 'dark'
? '#f0f0f0'
: '#111111',
color: isGreen ? CHROMA.GREEN.dark : '#000000',
px: 0.25,
borderRadius: 1,
'&:hover': {
bgcolor: 'action.hover',
},
})}
}}
>
<BuildRoundedIcon sx={{ fontSize: 18, color: 'inherit' }} />
{tool?.name ? (
@@ -294,7 +285,14 @@ function UsageChipView({ usage }) {
export function TurnRow({ turn, isLive = false }) {
if (!turn) return null;
const hasReasonings = Boolean(
turn.reasonings &&
turn.reasonings.some((r) => !r.sealed || Boolean(r.text && r.text.trim()))
);
const hasTools = Boolean(turn.tools && turn.tools.length > 0);
const hasContent = Boolean(turn.content);
const showHandshake = !hasReasonings && !hasTools && !hasContent;
const showStatusLine = showHandshake || hasReasonings || hasTools;
return (
<Box
@@ -309,26 +307,28 @@ export function TurnRow({ turn, isLive = false }) {
}}
>
{/* Turn status line: left of the chat */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
flexWrap: 'wrap',
minHeight: 24,
lineHeight: 1,
}}
>
<HandshakeIconView state={turn.handshake} />
{showStatusLine ? (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
flexWrap: 'wrap',
minHeight: 24,
lineHeight: 1,
}}
>
{showHandshake ? <HandshakeIconView state={turn.handshake} /> : null}
{turn.reasonings?.map((r, i) => (
<BrainIconView key={r.id || i} reasoning={r} />
))}
{turn.reasonings?.map((r, i) => (
<BrainIconView key={r.id || i} reasoning={r} />
))}
{turn.tools?.map((t, i) => (
<WorkIconView key={t.id || i} tool={t} />
))}
</Box>
{turn.tools?.map((t, i) => (
<WorkIconView key={t.id || i} tool={t} />
))}
</Box>
) : null}
{/* Assistant message content for this turn (if any) */}
{hasContent ? (
@@ -543,15 +543,8 @@ function MessageRow({ msg, showTime = true }) {
const payload = parseMessagePayload(msg.message);
const textContent = payload.content !== undefined ? payload.content : msg.message;
const toolCalls = Array.isArray(payload.tool_calls) && payload.tool_calls.length > 0 ? payload.tool_calls : null;
const isUser = msg.role === 'user';
const isSystem = msg.role === 'system';
const isTool = msg.role === 'tool';
const isReasoning = msg.role === 'reasoning' || Boolean(payload.reasoning_content && !toolCalls && payload.content === payload.reasoning_content);
const timeStr = showTime ? formatMessageTime(msg.created_at) : '';
if (isSystem) {
if (msg.role === 'system') {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 0.75, px: 2 }}>
<Box
@@ -573,174 +566,8 @@ function MessageRow({ msg, showTime = true }) {
);
}
if (isReasoning) {
if (!textContent?.trim()) return null;
const singleTurn = {
id: msg.id || 'r-turn',
handshake: 'green',
reasonings: [{ id: msg.id || 'r', text: textContent, sealed: true }],
tools: [],
};
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
<TurnRow turn={singleTurn} isLive={false} />
</Box>
);
}
if (toolCalls) {
const formattedCalls = toolCalls
.map((tc) => {
let args = {};
try {
if (typeof tc.function?.arguments === 'string') {
args = JSON.parse(tc.function.arguments);
} else if (tc.function?.arguments) {
args = tc.function.arguments;
}
} catch {}
if (tc.function?.name === 'ask_user') {
const q = args.question || '';
return `ask_user: ${q}`;
}
const method = args.method || (tc.function?.name ? tc.function.name.toUpperCase() : 'CALL');
const path = args.path || args.url || '';
if (path) {
return `${method} ${path}`;
}
return `${tc.function?.name || 'tool'} ${JSON.stringify(args)}`;
})
.join('\n');
return (
<Box sx={{ display: 'flex', justifyContent: 'flex-start', py: 0.35, px: 2 }}>
<Box
sx={(theme) => ({
bgcolor: byMode(theme, SURFACE_SUBTLE_PAIR),
color: 'text.secondary',
px: 1.25,
py: 0.35,
borderRadius: 2,
fontSize: '0.72rem',
maxWidth: '90%',
fontFamily: 'monospace',
border: `1px solid ${theme.palette.divider}`,
display: 'inline-flex',
alignItems: 'center',
gap: 1,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
})}
>
<Box
component="span"
sx={{
fontWeight: 600,
fontSize: '0.65rem',
textTransform: 'uppercase',
letterSpacing: 0.5,
color: 'text.disabled',
flexShrink: 0,
}}
>
Werkzeug
</Box>
<Box component="span" sx={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
{formattedCalls}
</Box>
</Box>
</Box>
);
}
if (isTool) {
let statusText = '';
let bodyLength = null;
let resObj = null;
if (typeof textContent === 'object' && textContent !== null) {
resObj = textContent;
} else if (typeof textContent === 'string') {
try {
const parsed = JSON.parse(textContent);
if (parsed && typeof parsed === 'object') {
resObj = parsed;
}
} catch {}
}
if (resObj && ('status' in resObj || 'statusCode' in resObj || 'body' in resObj)) {
const status = resObj.status ?? resObj.statusCode ?? 200;
statusText = `Status ${status}`;
if (typeof resObj.body === 'string') {
bodyLength = resObj.body.length;
} else if (resObj.body != null) {
bodyLength = JSON.stringify(resObj.body).length;
} else if (resObj.data != null) {
bodyLength = JSON.stringify(resObj.data).length;
}
} else if (typeof textContent === 'string') {
bodyLength = textContent.length;
}
const isToolAnswer =
Boolean(resObj) &&
('answer' in resObj || 'content' in resObj) &&
'tool_call_id' in resObj &&
!('status' in resObj);
const displayText = isToolAnswer
? resObj.answer || resObj.content || ''
: statusText
? `${statusText}${bodyLength != null ? ` · ${bodyLength} byte` : ''}`
: bodyLength != null
? `${bodyLength} byte`
: String(textContent);
return (
<Box sx={{ display: 'flex', justifyContent: 'flex-start', py: 0.35, px: 2 }}>
<Box
sx={(theme) => ({
bgcolor: byMode(theme, SURFACE_SUBTLE_PAIR),
color: 'text.secondary',
px: 1.25,
py: 0.35,
borderRadius: 2,
fontSize: '0.72rem',
maxWidth: '90%',
fontFamily: 'monospace',
border: `1px solid ${theme.palette.divider}`,
display: 'inline-flex',
alignItems: 'center',
gap: 1,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
})}
>
<Box
component="span"
sx={{
fontWeight: 600,
fontSize: '0.65rem',
textTransform: 'uppercase',
letterSpacing: 0.5,
color: 'text.disabled',
flexShrink: 0,
}}
>
Ergebnis
</Box>
<Box component="span" sx={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
{displayText}
</Box>
</Box>
</Box>
);
}
const isUser = msg.role === 'user';
const timeStr = showTime ? formatMessageTime(msg.created_at) : '';
return (
<Box