178 lines
4.4 KiB
TypeScript
178 lines
4.4 KiB
TypeScript
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,
|
|
},
|
|
}));
|