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 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
|
||||||
|
|||||||
Reference in New Issue
Block a user