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 BuildRoundedIcon from '@mui/icons-material/BuildRounded';
import HealingMarkdown from './HealingMarkdown'; import HealingMarkdown from './HealingMarkdown';
import { import {
CHROMA,
SHADOW_BUBBLE_ASSISTANT, SHADOW_BUBBLE_ASSISTANT,
SHADOW_BUBBLE_USER, SHADOW_BUBBLE_USER,
SURFACE_RAISED, SURFACE_RAISED,
SURFACE_SUBTLE_PAIR,
byMode,
} from '../../colors'; } from '../../colors';
function formatMessageTime(isoString) { function formatMessageTime(isoString) {
@@ -47,44 +46,42 @@ function formatUsageText(usage) {
} }
function formatToolTooltip(content, result, parameters) { function formatToolTooltip(content, result, parameters) {
let text = ''; const parts = [];
if (parameters !== undefined && parameters !== null && parameters !== '') { if (parameters !== undefined && parameters !== null && parameters !== '') {
try { try {
const parsed = typeof parameters === 'string' ? JSON.parse(parameters) : parameters; 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 { } catch {
text = 'Aufruf:\n' + String(parameters); parts.push(String(parameters));
} }
} }
let contentText = '';
if (content !== undefined && content !== null && content !== '') { if (content !== undefined && content !== null && content !== '') {
try { try {
const parsed = typeof content === 'string' ? JSON.parse(content) : content; 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 { } catch {
contentText = String(content); const str = String(content);
if (!parts.includes(str)) {
parts.push(str);
} }
} }
if (contentText) {
if (text) {
text += '\n\nErgebnis:\n' + contentText;
} else {
text = contentText;
}
} }
if (result !== undefined && result !== null) { if (result !== undefined && result !== null) {
try { try {
const resStr = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result); const resStr = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result);
if (!text.includes(resStr)) { if (!parts.some((p) => p.includes(resStr))) {
text = text ? text + '\n\n' + resStr : resStr; parts.push(resStr);
} }
} catch {} } catch {}
} }
return text || 'Werkzeug wird ausgeführt…'; return parts.join('\n\n') || '';
} }
class ClickTooltip extends PureComponent { class ClickTooltip extends PureComponent {
@@ -110,6 +107,10 @@ class ClickTooltip extends PureComponent {
disableFocusListener disableFocusListener
disableHoverListener disableHoverListener
disableTouchListener disableTouchListener
enterDelay={0}
leaveDelay={0}
TransitionProps={{ timeout: 0 }}
slotProps={{ transition: { timeout: 0 } }}
title={title} title={title}
arrow arrow
placement="top" placement="top"
@@ -138,23 +139,19 @@ function HandshakeIconView({ state }) {
return ( return (
<Box <Box
sx={(theme) => ({ sx={{
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
width: 24, width: 24,
height: 24, height: 24,
color: isGreen color: isGreen
? theme.palette.mode === 'dark' ? CHROMA.GREEN.dark
? '#66bb6a'
: '#2e7d32'
: isBlack : isBlack
? theme.palette.mode === 'dark' ? '#000000'
? '#f0f0f0'
: '#111111'
: '#9e9e9e', : '#9e9e9e',
transition: 'color 180ms ease-in-out', transition: 'color 180ms ease-in-out',
})} }}
> >
<HandshakeRoundedIcon sx={{ fontSize: 20, color: 'inherit' }} /> <HandshakeRoundedIcon sx={{ fontSize: 20, color: 'inherit' }} />
</Box> </Box>
@@ -163,7 +160,7 @@ function HandshakeIconView({ state }) {
function BrainIconView({ reasoning }) { function BrainIconView({ reasoning }) {
const tooltipContent = reasoning?.text?.trim() || ''; const tooltipContent = reasoning?.text?.trim() || '';
if (!tooltipContent) return null; if (!tooltipContent && reasoning?.sealed) return null;
return ( return (
<ClickTooltip <ClickTooltip
@@ -179,23 +176,23 @@ function BrainIconView({ reasoning }) {
lineHeight: 1.45, lineHeight: 1.45,
}} }}
> >
{tooltipContent} {tooltipContent || 'Wird überlegt…'}
</Box> </Box>
} }
> >
<Box <Box
sx={(theme) => ({ sx={{
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
width: 24, width: 24,
height: 24, height: 24,
color: theme.palette.mode === 'dark' ? '#b388ff' : '#651fff', color: CHROMA.PURPLE.dark,
borderRadius: 1, borderRadius: 1,
'&:hover': { '&:hover': {
bgcolor: 'action.hover', bgcolor: 'action.hover',
}, },
})} }}
> >
<PsychologyRoundedIcon sx={{ fontSize: 20, color: 'inherit' }} /> <PsychologyRoundedIcon sx={{ fontSize: 20, color: 'inherit' }} />
</Box> </Box>
@@ -227,24 +224,18 @@ function WorkIconView({ tool }) {
} }
> >
<Box <Box
sx={(theme) => ({ sx={{
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
height: 24, height: 24,
gap: 0.5, gap: 0.5,
color: isGreen color: isGreen ? CHROMA.GREEN.dark : '#000000',
? theme.palette.mode === 'dark'
? '#66bb6a'
: '#2e7d32'
: theme.palette.mode === 'dark'
? '#f0f0f0'
: '#111111',
px: 0.25, px: 0.25,
borderRadius: 1, borderRadius: 1,
'&:hover': { '&:hover': {
bgcolor: 'action.hover', bgcolor: 'action.hover',
}, },
})} }}
> >
<BuildRoundedIcon sx={{ fontSize: 18, color: 'inherit' }} /> <BuildRoundedIcon sx={{ fontSize: 18, color: 'inherit' }} />
{tool?.name ? ( {tool?.name ? (
@@ -294,7 +285,14 @@ function UsageChipView({ usage }) {
export function TurnRow({ turn, isLive = false }) { export function TurnRow({ turn, isLive = false }) {
if (!turn) return null; 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 hasContent = Boolean(turn.content);
const showHandshake = !hasReasonings && !hasTools && !hasContent;
const showStatusLine = showHandshake || hasReasonings || hasTools;
return ( return (
<Box <Box
@@ -309,6 +307,7 @@ export function TurnRow({ turn, isLive = false }) {
}} }}
> >
{/* Turn status line: left of the chat */} {/* Turn status line: left of the chat */}
{showStatusLine ? (
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -319,7 +318,7 @@ export function TurnRow({ turn, isLive = false }) {
lineHeight: 1, lineHeight: 1,
}} }}
> >
<HandshakeIconView state={turn.handshake} /> {showHandshake ? <HandshakeIconView state={turn.handshake} /> : null}
{turn.reasonings?.map((r, i) => ( {turn.reasonings?.map((r, i) => (
<BrainIconView key={r.id || i} reasoning={r} /> <BrainIconView key={r.id || i} reasoning={r} />
@@ -329,6 +328,7 @@ export function TurnRow({ turn, isLive = false }) {
<WorkIconView key={t.id || i} tool={t} /> <WorkIconView key={t.id || i} tool={t} />
))} ))}
</Box> </Box>
) : null}
{/* Assistant message content for this turn (if any) */} {/* Assistant message content for this turn (if any) */}
{hasContent ? ( {hasContent ? (
@@ -543,15 +543,8 @@ function MessageRow({ msg, showTime = true }) {
const payload = parseMessagePayload(msg.message); const payload = parseMessagePayload(msg.message);
const textContent = payload.content !== undefined ? payload.content : 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'; if (msg.role === 'system') {
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) {
return ( return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 0.75, px: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'center', py: 0.75, px: 2 }}>
<Box <Box
@@ -573,174 +566,8 @@ function MessageRow({ msg, showTime = true }) {
); );
} }
if (isReasoning) { const isUser = msg.role === 'user';
if (!textContent?.trim()) return null; const timeStr = showTime ? formatMessageTime(msg.created_at) : '';
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>
);
}
return ( return (
<Box <Box