u
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import React, { PureComponent, createRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import HandshakeRoundedIcon from '@mui/icons-material/HandshakeRounded';
|
||||
import PsychologyRoundedIcon from '@mui/icons-material/PsychologyRounded';
|
||||
import BuildRoundedIcon from '@mui/icons-material/BuildRounded';
|
||||
import HealingMarkdown from './HealingMarkdown';
|
||||
import {
|
||||
SHADOW_BUBBLE_ASSISTANT,
|
||||
@@ -30,7 +35,511 @@ function parseMessagePayload(raw) {
|
||||
return { content: raw };
|
||||
}
|
||||
|
||||
function formatUsageText(usage) {
|
||||
if (!usage) return '';
|
||||
const input = Number(usage.input ?? usage.prompt_tokens ?? 0);
|
||||
const output = Number(usage.output ?? usage.completion_tokens ?? 0);
|
||||
const cached = Number(usage.cached ?? 0);
|
||||
const cost = typeof usage.cost === 'number' ? ` · $${usage.cost.toFixed(6)}` : '';
|
||||
const fmt = (n) => n.toLocaleString('de-DE');
|
||||
const cachedPart = cached > 0 ? ` / ${fmt(cached)} Cache` : '';
|
||||
return `${fmt(input)} Prompt${cachedPart} · ${fmt(output)} Antwort${cost}`;
|
||||
}
|
||||
|
||||
function formatToolTooltip(content, result, parameters) {
|
||||
let text = '';
|
||||
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));
|
||||
} catch {
|
||||
text = 'Aufruf:\n' + 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);
|
||||
} catch {
|
||||
contentText = String(content);
|
||||
}
|
||||
}
|
||||
|
||||
if (contentText) {
|
||||
if (text) {
|
||||
text += '\n\nErgebnis:\n' + contentText;
|
||||
} else {
|
||||
text = contentText;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return text || 'Werkzeug wird ausgeführt…';
|
||||
}
|
||||
|
||||
class ClickTooltip extends PureComponent {
|
||||
state = { open: false };
|
||||
|
||||
handleClose = () => {
|
||||
if (this.state.open) this.setState({ open: false });
|
||||
};
|
||||
|
||||
handleToggle = (e) => {
|
||||
e?.stopPropagation();
|
||||
this.setState((prev) => ({ open: !prev.open }));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { title, children } = this.props;
|
||||
return (
|
||||
<ClickAwayListener onClickAway={this.handleClose}>
|
||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', height: 24 }}>
|
||||
<Tooltip
|
||||
open={this.state.open}
|
||||
onClose={this.handleClose}
|
||||
disableFocusListener
|
||||
disableHoverListener
|
||||
disableTouchListener
|
||||
title={title}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Box
|
||||
onClick={this.handleToggle}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
height: 24,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</ClickAwayListener>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function HandshakeIconView({ state }) {
|
||||
const isGreen = state === 'green';
|
||||
const isBlack = state === 'black';
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: isGreen
|
||||
? theme.palette.mode === 'dark'
|
||||
? '#66bb6a'
|
||||
: '#2e7d32'
|
||||
: isBlack
|
||||
? theme.palette.mode === 'dark'
|
||||
? '#f0f0f0'
|
||||
: '#111111'
|
||||
: '#9e9e9e',
|
||||
transition: 'color 180ms ease-in-out',
|
||||
})}
|
||||
>
|
||||
<HandshakeRoundedIcon sx={{ fontSize: 20, color: 'inherit' }} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function BrainIconView({ reasoning }) {
|
||||
const tooltipContent = reasoning?.text?.trim() || '';
|
||||
if (!tooltipContent) return null;
|
||||
|
||||
return (
|
||||
<ClickTooltip
|
||||
title={
|
||||
<Box
|
||||
sx={{
|
||||
p: 0.5,
|
||||
maxWidth: 440,
|
||||
maxHeight: 280,
|
||||
overflowY: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: 1.45,
|
||||
}}
|
||||
>
|
||||
{tooltipContent}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: theme.palette.mode === 'dark' ? '#b388ff' : '#651fff',
|
||||
borderRadius: 1,
|
||||
'&:hover': {
|
||||
bgcolor: 'action.hover',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<PsychologyRoundedIcon sx={{ fontSize: 20, color: 'inherit' }} />
|
||||
</Box>
|
||||
</ClickTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkIconView({ tool }) {
|
||||
const isGreen = tool?.state === 'green';
|
||||
const tooltipText = formatToolTooltip(tool?.content, tool?.result, tool?.parameters);
|
||||
|
||||
return (
|
||||
<ClickTooltip
|
||||
title={
|
||||
<Box
|
||||
sx={{
|
||||
p: 0.5,
|
||||
maxWidth: 480,
|
||||
maxHeight: 300,
|
||||
overflowY: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.72rem',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{tooltipText}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
height: 24,
|
||||
gap: 0.5,
|
||||
color: isGreen
|
||||
? theme.palette.mode === 'dark'
|
||||
? '#66bb6a'
|
||||
: '#2e7d32'
|
||||
: theme.palette.mode === 'dark'
|
||||
? '#f0f0f0'
|
||||
: '#111111',
|
||||
px: 0.25,
|
||||
borderRadius: 1,
|
||||
'&:hover': {
|
||||
bgcolor: 'action.hover',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<BuildRoundedIcon sx={{ fontSize: 18, color: 'inherit' }} />
|
||||
{tool?.name ? (
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 600,
|
||||
color: 'text.secondary',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{tool.name}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
</ClickTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function UsageChipView({ usage }) {
|
||||
const text = formatUsageText(usage);
|
||||
if (!text) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
fontSize: '0.68rem',
|
||||
fontFamily: 'monospace',
|
||||
color: 'text.secondary',
|
||||
bgcolor: 'action.hover',
|
||||
px: 0.8,
|
||||
py: 0.25,
|
||||
borderRadius: 1.5,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function TurnRow({ turn, isLive = false }) {
|
||||
if (!turn) return null;
|
||||
|
||||
const hasContent = Boolean(turn.content);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
px: 2,
|
||||
py: 0.4,
|
||||
gap: 0.75,
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
{/* 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} />
|
||||
|
||||
{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>
|
||||
|
||||
{/* Assistant message content for this turn (if any) */}
|
||||
{hasContent ? (
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
maxWidth: '85%',
|
||||
px: 2,
|
||||
py: 1.25,
|
||||
borderRadius: '16px 16px 16px 4px',
|
||||
bgcolor:
|
||||
theme.palette.mode === 'dark'
|
||||
? 'background.paper'
|
||||
: SURFACE_RAISED,
|
||||
color: theme.palette.text.primary,
|
||||
boxShadow: SHADOW_BUBBLE_ASSISTANT,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
wordBreak: 'break-word',
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: 1.5,
|
||||
})}
|
||||
>
|
||||
<HealingMarkdown text={turn.content} streaming={isLive && !turn.sealed} />
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{/* Pricing / usage is always on a new line */}
|
||||
{turn.usage ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<UsageChipView usage={turn.usage} />
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeChatMessages(rawMessages = []) {
|
||||
if (!Array.isArray(rawMessages) || rawMessages.length === 0) return [];
|
||||
|
||||
const result = [];
|
||||
let currentAssistantTurns = [];
|
||||
let currentTurn = null;
|
||||
|
||||
const flushAssistant = () => {
|
||||
if (currentTurn) {
|
||||
if (
|
||||
(currentTurn.reasonings && currentTurn.reasonings.length > 0) ||
|
||||
(currentTurn.tools && currentTurn.tools.length > 0) ||
|
||||
currentTurn.content ||
|
||||
currentTurn.usage
|
||||
) {
|
||||
currentAssistantTurns.push(currentTurn);
|
||||
}
|
||||
currentTurn = null;
|
||||
}
|
||||
if (currentAssistantTurns.length > 0) {
|
||||
const fullText = currentAssistantTurns
|
||||
.map((t) => t.content)
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
|
||||
result.push({
|
||||
id: currentAssistantTurns[0]?.id ? `asst-${currentAssistantTurns[0].id}` : `asst-${result.length}`,
|
||||
role: 'assistant',
|
||||
message: fullText,
|
||||
turns: currentAssistantTurns,
|
||||
created_at: currentAssistantTurns[0]?.created_at,
|
||||
});
|
||||
currentAssistantTurns = [];
|
||||
}
|
||||
};
|
||||
|
||||
let turnSeq = 0;
|
||||
const getOrCreateTurn = (msg) => {
|
||||
const shouldStartNew =
|
||||
!currentTurn ||
|
||||
currentTurn.sealed ||
|
||||
(msg?.role === 'reasoning' && (currentTurn.tools.length > 0 || Boolean(currentTurn.content))) ||
|
||||
(msg?.role === 'tool' && Boolean(currentTurn.content));
|
||||
|
||||
if (!currentTurn) {
|
||||
turnSeq++;
|
||||
currentTurn = {
|
||||
id: msg?.id ? `turn-msg-${msg.id}` : `turn-${result.length}-${turnSeq}`,
|
||||
handshake: 'green',
|
||||
reasonings: [],
|
||||
tools: [],
|
||||
content: '',
|
||||
usage: null,
|
||||
sealed: false,
|
||||
created_at: msg?.created_at,
|
||||
};
|
||||
} else if (shouldStartNew) {
|
||||
currentAssistantTurns.push(currentTurn);
|
||||
turnSeq++;
|
||||
currentTurn = {
|
||||
id: msg?.id ? `turn-msg-${msg.id}` : `turn-${result.length}-${turnSeq}`,
|
||||
handshake: 'green',
|
||||
reasonings: [],
|
||||
tools: [],
|
||||
content: '',
|
||||
usage: null,
|
||||
sealed: false,
|
||||
created_at: msg?.created_at,
|
||||
};
|
||||
}
|
||||
return currentTurn;
|
||||
};
|
||||
|
||||
for (const msg of rawMessages) {
|
||||
if (!msg) continue;
|
||||
|
||||
if (msg.turns && Array.isArray(msg.turns) && msg.turns.length > 0) {
|
||||
flushAssistant();
|
||||
result.push(msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
const payload = parseMessagePayload(msg.message);
|
||||
|
||||
if (msg.role === 'user') {
|
||||
flushAssistant();
|
||||
result.push({
|
||||
...msg,
|
||||
message: payload.content !== undefined ? payload.content : msg.message,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.role === 'reasoning') {
|
||||
const turn = getOrCreateTurn(msg);
|
||||
const text =
|
||||
payload.plain ||
|
||||
payload.text ||
|
||||
payload.content ||
|
||||
payload.thinking ||
|
||||
payload.reasoning ||
|
||||
(typeof msg.message === 'string' ? msg.message : '');
|
||||
turn.reasonings.push({
|
||||
id: msg.id || `r-${turn.reasonings.length}`,
|
||||
text,
|
||||
sealed: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.role === 'tool') {
|
||||
const turn = getOrCreateTurn(msg);
|
||||
turn.tools.push({
|
||||
id: payload.tool_call_id || msg.id,
|
||||
state: 'green',
|
||||
name: payload.name || '',
|
||||
content: payload.content !== undefined ? payload.content : (payload.parameters || ''),
|
||||
parameters: payload.parameters,
|
||||
result: payload.result,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.role === 'usage') {
|
||||
const turn = getOrCreateTurn(msg);
|
||||
turn.usage = payload;
|
||||
turn.sealed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.role === 'assistant' || msg.role === 'model') {
|
||||
const toolCalls = Array.isArray(payload.tool_calls) && payload.tool_calls.length > 0 ? payload.tool_calls : null;
|
||||
if (toolCalls) {
|
||||
const turn = getOrCreateTurn(msg);
|
||||
for (const tc of toolCalls) {
|
||||
let args = {};
|
||||
try {
|
||||
args = typeof tc.function?.arguments === 'string' ? JSON.parse(tc.function.arguments) : (tc.function?.arguments || {});
|
||||
} catch {}
|
||||
turn.tools.push({
|
||||
id: tc.id || `tc-${Date.now()}`,
|
||||
state: 'green',
|
||||
name: tc.function?.name || 'tool',
|
||||
content: JSON.stringify(args, null, 2),
|
||||
result: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const content = payload.content !== undefined ? payload.content : msg.message;
|
||||
if (content && typeof content === 'string') {
|
||||
const turn = getOrCreateTurn(msg);
|
||||
turn.content = content;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
flushAssistant();
|
||||
result.push(msg);
|
||||
}
|
||||
|
||||
flushAssistant();
|
||||
return result;
|
||||
}
|
||||
|
||||
function MessageRow({ msg, showTime = true }) {
|
||||
// If the message has structured turns, render each turn
|
||||
if (msg.turns && Array.isArray(msg.turns) && msg.turns.length > 0) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
|
||||
{msg.turns.map((t, idx) => (
|
||||
<TurnRow key={t.id || idx} turn={t} isLive={false} />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const payload = parseMessagePayload(msg.message);
|
||||
const textContent = payload.content !== undefined ? payload.content : msg.message;
|
||||
@@ -39,7 +548,7 @@ function MessageRow({ msg, showTime = true }) {
|
||||
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 isReasoning = msg.role === 'reasoning' || Boolean(payload.reasoning_content && !toolCalls && payload.content === payload.reasoning_content);
|
||||
const timeStr = showTime ? formatMessageTime(msg.created_at) : '';
|
||||
|
||||
if (isSystem) {
|
||||
@@ -65,52 +574,45 @@ 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', justifyContent: 'flex-start', py: 0.5, px: 2 }}>
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
bgcolor: 'transparent',
|
||||
color: 'text.disabled',
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
borderRadius: 2,
|
||||
fontSize: '0.75rem',
|
||||
fontStyle: 'italic',
|
||||
maxWidth: '85%',
|
||||
borderLeft: `2px solid ${theme.palette.divider}`,
|
||||
whiteSpace: 'pre-wrap',
|
||||
})}
|
||||
>
|
||||
<Box sx={{ fontWeight: 600, fontSize: '0.68rem', mb: 0.25 }}>Gedankengang</Box>
|
||||
{textContent}
|
||||
</Box>
|
||||
<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;
|
||||
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}`;
|
||||
}
|
||||
} 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');
|
||||
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 }}>
|
||||
@@ -194,7 +696,9 @@ function MessageRow({ msg, showTime = true }) {
|
||||
? resObj.answer || resObj.content || ''
|
||||
: statusText
|
||||
? `${statusText}${bodyLength != null ? ` · ${bodyLength} byte` : ''}`
|
||||
: (bodyLength != null ? `${bodyLength} byte` : String(textContent));
|
||||
: bodyLength != null
|
||||
? `${bodyLength} byte`
|
||||
: String(textContent);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-start', py: 0.35, px: 2 }}>
|
||||
@@ -280,12 +784,8 @@ function MessageRow({ msg, showTime = true }) {
|
||||
: theme.palette.mode === 'dark'
|
||||
? 'background.paper'
|
||||
: SURFACE_RAISED,
|
||||
color: isUser
|
||||
? theme.palette.primary.contrastText
|
||||
: theme.palette.text.primary,
|
||||
boxShadow: isUser
|
||||
? SHADOW_BUBBLE_USER
|
||||
: SHADOW_BUBBLE_ASSISTANT,
|
||||
color: isUser ? theme.palette.primary.contrastText : theme.palette.text.primary,
|
||||
boxShadow: isUser ? SHADOW_BUBBLE_USER : SHADOW_BUBBLE_ASSISTANT,
|
||||
border: isUser ? 'none' : `1px solid ${theme.palette.divider}`,
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: isUser ? 'pre-wrap' : 'normal',
|
||||
@@ -316,7 +816,8 @@ export default class ChatFlowList extends PureComponent {
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
this.props.messages !== prevProps.messages ||
|
||||
this.props.messages?.length !== prevProps.messages?.length
|
||||
this.props.messages?.length !== prevProps.messages?.length ||
|
||||
this.props.activeTurns !== prevProps.activeTurns
|
||||
) {
|
||||
this.scrollToBottom('smooth');
|
||||
}
|
||||
@@ -348,9 +849,10 @@ export default class ChatFlowList extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { messages = [] } = this.props;
|
||||
const { messages = [], activeTurns = [] } = this.props;
|
||||
const normalizedMessages = normalizeChatMessages(messages);
|
||||
|
||||
if (messages.length === 0) {
|
||||
if (normalizedMessages.length === 0 && activeTurns.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -370,8 +872,8 @@ export default class ChatFlowList extends PureComponent {
|
||||
gap: 0.25,
|
||||
}}
|
||||
>
|
||||
{messages.map((msg, index) => {
|
||||
const prevMsg = index > 0 ? messages[index - 1] : null;
|
||||
{normalizedMessages.map((msg, index) => {
|
||||
const prevMsg = index > 0 ? normalizedMessages[index - 1] : null;
|
||||
const currTime = formatMessageTime(msg.created_at);
|
||||
const prevTime = prevMsg ? formatMessageTime(prevMsg.created_at) : '';
|
||||
const showTime = Boolean(currTime && currTime !== prevTime);
|
||||
@@ -384,6 +886,14 @@ export default class ChatFlowList extends PureComponent {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Live streaming turns */}
|
||||
{activeTurns && activeTurns.length > 0
|
||||
? activeTurns.map((turn, idx) => (
|
||||
<TurnRow key={turn.id || idx} turn={turn} isLive={true} />
|
||||
))
|
||||
: null}
|
||||
|
||||
<div ref={this.bottomRef} style={{ height: 1, flexShrink: 0 }} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user