u
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user