u
This commit is contained in:
@@ -719,7 +719,7 @@ function UsageChipView({ usage, isSummary = false }) {
|
||||
fontSize: '0.68rem',
|
||||
fontFamily: 'monospace',
|
||||
color: isSummary ? 'text.primary' : 'text.secondary',
|
||||
fontWeight: isSummary ? 600 : 400,
|
||||
fontWeight: isSummary ? 400 : 400,
|
||||
bgcolor: isSummary ? 'action.selected' : 'action.hover',
|
||||
border: isSummary ? '1px solid' : 'none',
|
||||
borderColor: isSummary ? 'divider' : 'transparent',
|
||||
@@ -777,86 +777,159 @@ function UsageIconView({ usage }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function TurnRow({ turn, isLive = false }) {
|
||||
if (!turn) return null;
|
||||
|
||||
function turnHasIcons(turn) {
|
||||
if (!turn) return false;
|
||||
const hasReasonings = Boolean(
|
||||
turn.reasonings &&
|
||||
turn.reasonings.some((r) => Boolean(r.text && r.text.trim()))
|
||||
);
|
||||
const hasTools = Boolean(turn.tools && turn.tools.length > 0);
|
||||
const hasContent = Boolean(turn.content);
|
||||
const hasUsage = Boolean(turn.usage);
|
||||
const hasContent = Boolean(turn.content);
|
||||
const showHandshake = !hasReasonings && !hasTools && !hasContent && !hasUsage;
|
||||
const showStatusLine = showHandshake || hasReasonings || hasTools || hasUsage;
|
||||
return showHandshake || hasReasonings || hasTools || hasUsage;
|
||||
}
|
||||
|
||||
export function TurnIcons({ turn }) {
|
||||
if (!turn) return null;
|
||||
const hasReasonings = Boolean(
|
||||
turn.reasonings &&
|
||||
turn.reasonings.some((r) => Boolean(r.text && r.text.trim()))
|
||||
);
|
||||
const hasTools = Boolean(turn.tools && turn.tools.length > 0);
|
||||
const hasUsage = Boolean(turn.usage);
|
||||
const hasContent = Boolean(turn.content);
|
||||
const showHandshake = !hasReasonings && !hasTools && !hasContent && !hasUsage;
|
||||
|
||||
const showStatus = showHandshake || hasReasonings || hasTools || hasUsage;
|
||||
if (!showStatus) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
px: 2,
|
||||
py: 0.4,
|
||||
gap: 0.75,
|
||||
maxWidth: '100%',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
}}
|
||||
>
|
||||
{/* Turn status line: left of the chat */}
|
||||
{showStatusLine ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
flexWrap: 'wrap',
|
||||
minHeight: 24,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{showHandshake ? <HandshakeIconView state={turn.handshake} /> : null}
|
||||
{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} />
|
||||
))}
|
||||
{turn.tools?.map((t, i) => (
|
||||
<WorkIconView key={t.id || i} tool={t} />
|
||||
))}
|
||||
|
||||
{hasUsage ? (
|
||||
<UsageIconView usage={turn.usage} />
|
||||
) : null}
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{/* 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>
|
||||
{hasUsage ? (
|
||||
<UsageIconView usage={turn.usage} />
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function TurnsView({ turns = [], isLive = false }) {
|
||||
if (!Array.isArray(turns) || turns.length === 0) return null;
|
||||
|
||||
// Segment turns into chunks where icons precede content
|
||||
const segments = [];
|
||||
let pendingTurns = [];
|
||||
|
||||
for (let i = 0; i < turns.length; i++) {
|
||||
const turn = turns[i];
|
||||
pendingTurns.push(turn);
|
||||
|
||||
if (turn.content) {
|
||||
segments.push({
|
||||
statusTurns: [...pendingTurns],
|
||||
contentTurn: turn,
|
||||
});
|
||||
pendingTurns = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingTurns.length > 0) {
|
||||
segments.push({
|
||||
statusTurns: pendingTurns,
|
||||
contentTurn: null,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75, width: '100%' }}>
|
||||
{segments.map((seg, sIdx) => {
|
||||
const hasAnyIcons = seg.statusTurns.some(turnHasIcons);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={sIdx}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
px: 2,
|
||||
py: 0.4,
|
||||
gap: 0.75,
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
{/* All turn icons in ONE line */}
|
||||
{hasAnyIcons ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
flexWrap: 'wrap',
|
||||
minHeight: 24,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{seg.statusTurns.map((t, tIdx) => (
|
||||
<TurnIcons key={t.id || tIdx} turn={t} />
|
||||
))}
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{/* Assistant message content for this segment (if any) */}
|
||||
{seg.contentTurn?.content ? (
|
||||
<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={seg.contentTurn.content}
|
||||
streaming={isLive && !seg.contentTurn.sealed}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function TurnRow({ turn, isLive = false }) {
|
||||
if (!turn) return null;
|
||||
return <TurnsView turns={[turn]} isLive={isLive} />;
|
||||
}
|
||||
|
||||
export function normalizeChatMessages(rawMessages = []) {
|
||||
if (!Array.isArray(rawMessages) || rawMessages.length === 0) return [];
|
||||
|
||||
@@ -1063,9 +1136,7 @@ function MessageRow({ msg, showTime = true }) {
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
|
||||
{msg.turns.map((t, idx) => (
|
||||
<TurnRow key={t.id || idx} turn={t} isLive={false} />
|
||||
))}
|
||||
<TurnsView turns={msg.turns} isLive={false} />
|
||||
{summaryUsage ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', px: 2, py: 0.4 }}>
|
||||
<UsageChipView
|
||||
@@ -1309,9 +1380,7 @@ 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} />
|
||||
))}
|
||||
<TurnsView turns={activeTurns} isLive={true} />
|
||||
{(() => {
|
||||
const activeSummary = aggregateUsage(activeTurns);
|
||||
if (!activeSummary) return null;
|
||||
|
||||
Reference in New Issue
Block a user