refactor(app): organize agent stream rendering
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import React, { memo, useCallback, useMemo, type ReactNode } from "react";
|
||||
import { View } from "react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import type { TurnTiming } from "@/timeline/turn-time";
|
||||
import type { StreamItem } from "@/types/stream";
|
||||
import {
|
||||
collectAssistantTurnContentForStreamRenderStrategy,
|
||||
type StreamStrategy,
|
||||
} from "./strategy";
|
||||
import { AssistantTurnFooter, LiveElapsed, STREAM_METADATA_FONT_SIZE } from "@/components/message";
|
||||
import type { TurnFooterHost } from "./layout";
|
||||
import { SyncedLoader } from "@/components/synced-loader";
|
||||
|
||||
const ThemedSyncedLoader = withUnistyles(SyncedLoader);
|
||||
const workingIndicatorColorMapping = (theme: Theme) => ({
|
||||
color:
|
||||
theme.colorScheme === "light"
|
||||
? theme.colors.palette.amber[700]
|
||||
: theme.colors.palette.amber[500],
|
||||
});
|
||||
|
||||
export type TurnContentStrategy = StreamStrategy;
|
||||
|
||||
export const TurnFooter = memo(function TurnFooter({
|
||||
isRunning,
|
||||
inFlightTurnStartedAt,
|
||||
host,
|
||||
strategy,
|
||||
}: {
|
||||
isRunning: boolean;
|
||||
inFlightTurnStartedAt: Date | null;
|
||||
host: TurnFooterHost | null;
|
||||
strategy: TurnContentStrategy;
|
||||
}) {
|
||||
if (isRunning) {
|
||||
return (
|
||||
<TurnFooterRow>
|
||||
<RunningTurnFooter inFlightTurnStartedAt={inFlightTurnStartedAt} />
|
||||
</TurnFooterRow>
|
||||
);
|
||||
}
|
||||
if (!host) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<CompletedTurnFooterRow
|
||||
strategy={strategy}
|
||||
items={host.items}
|
||||
timing={host.timing}
|
||||
startIndex={host.startIndex}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const CompletedTurnFooterRow = memo(function CompletedTurnFooterRow({
|
||||
strategy,
|
||||
items,
|
||||
timing,
|
||||
startIndex,
|
||||
}: {
|
||||
strategy: TurnContentStrategy;
|
||||
items: StreamItem[];
|
||||
timing?: TurnTiming;
|
||||
startIndex: number;
|
||||
}) {
|
||||
return (
|
||||
<TurnFooterRow>
|
||||
<CompletedTurnFooter
|
||||
strategy={strategy}
|
||||
items={items}
|
||||
timing={timing}
|
||||
startIndex={startIndex}
|
||||
/>
|
||||
</TurnFooterRow>
|
||||
);
|
||||
});
|
||||
|
||||
const WorkingIndicator = memo(function WorkingIndicator({
|
||||
inFlightTurnStartedAt = null,
|
||||
}: {
|
||||
inFlightTurnStartedAt?: Date | null;
|
||||
}) {
|
||||
return (
|
||||
<View style={stylesheet.turnFooterContent}>
|
||||
<View style={stylesheet.workingLoader}>
|
||||
<ThemedSyncedLoader size={14} uniProps={workingIndicatorColorMapping} />
|
||||
</View>
|
||||
{inFlightTurnStartedAt ? (
|
||||
<LiveElapsed
|
||||
startedAt={inFlightTurnStartedAt}
|
||||
style={stylesheet.workingElapsed}
|
||||
testID="turn-working-elapsed"
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
function RunningTurnFooter({ inFlightTurnStartedAt }: { inFlightTurnStartedAt: Date | null }) {
|
||||
return (
|
||||
<View style={stylesheet.turnFooterSlot} testID="turn-working-indicator">
|
||||
<WorkingIndicator inFlightTurnStartedAt={inFlightTurnStartedAt} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function CompletedTurnFooter({
|
||||
strategy,
|
||||
items,
|
||||
timing,
|
||||
startIndex,
|
||||
}: {
|
||||
strategy: TurnContentStrategy;
|
||||
items: StreamItem[];
|
||||
timing?: TurnTiming;
|
||||
startIndex: number;
|
||||
}) {
|
||||
const getContent = useCallback(
|
||||
() =>
|
||||
collectAssistantTurnContentForStreamRenderStrategy({
|
||||
strategy,
|
||||
items,
|
||||
startIndex,
|
||||
}),
|
||||
[strategy, items, startIndex],
|
||||
);
|
||||
return (
|
||||
<View style={stylesheet.turnFooterSlot}>
|
||||
<AssistantTurnFooter
|
||||
getContent={getContent}
|
||||
completedAt={timing?.completedAt}
|
||||
durationMs={timing?.durationMs}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function TurnFooterRow({ children }: { children: ReactNode }) {
|
||||
const rowStyle = useMemo(() => [stylesheet.streamItemWrapper, stylesheet.turnFooterRow], []);
|
||||
return <View style={rowStyle}>{children}</View>;
|
||||
}
|
||||
|
||||
const stylesheet = StyleSheet.create((theme) => ({
|
||||
streamItemWrapper: {
|
||||
width: "100%",
|
||||
maxWidth: MAX_CONTENT_WIDTH,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
},
|
||||
turnFooterRow: {
|
||||
marginTop: theme.spacing[4],
|
||||
},
|
||||
turnFooterSlot: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
alignSelf: "flex-start",
|
||||
minHeight: 24,
|
||||
paddingBottom: theme.spacing[6],
|
||||
},
|
||||
turnFooterContent: {
|
||||
height: 24,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
gap: theme.spacing[3],
|
||||
},
|
||||
workingElapsed: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: STREAM_METADATA_FONT_SIZE,
|
||||
fontVariant: ["tabular-nums"],
|
||||
},
|
||||
workingLoader: {
|
||||
marginLeft: -2,
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user