mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Match thinking badge behavior to tool badge
This commit is contained in:
@@ -49,6 +49,7 @@ import type { DaemonClientV2 } from "@server/client/daemon-client-v2";
|
||||
import { parseToolCallDisplay } from "@/utils/tool-call-parsers";
|
||||
import { ToolCallDetailsContent } from "./tool-call-details";
|
||||
import { ToolCallSheetProvider } from "./tool-call-sheet";
|
||||
import { ThinkingSheetProvider } from "./thinking-sheet";
|
||||
import { createMarkdownStyles } from "@/styles/markdown-styles";
|
||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
||||
@@ -532,62 +533,64 @@ export function AgentStreamView({
|
||||
|
||||
return (
|
||||
<ToolCallSheetProvider>
|
||||
<View style={stylesheet.container}>
|
||||
<MessageOuterSpacingProvider disableOuterSpacing>
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
data={flatListData}
|
||||
renderItem={renderStreamItem}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={{
|
||||
paddingVertical: 0,
|
||||
flexGrow: 1,
|
||||
}}
|
||||
style={stylesheet.list}
|
||||
onScroll={handleScroll}
|
||||
scrollEventThrottle={16}
|
||||
ListEmptyComponent={
|
||||
<View style={[stylesheet.emptyState, stylesheet.contentWrapper]}>
|
||||
<Text style={stylesheet.emptyStateText}>
|
||||
Start chatting with this agent...
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
ListHeaderComponent={listHeaderComponent}
|
||||
extraData={flatListExtraData}
|
||||
maintainVisibleContentPosition={
|
||||
// Disable when streaming and user is at bottom - we handle auto-scroll ourselves
|
||||
agent.status === "running" && isNearBottom
|
||||
? undefined
|
||||
: { minIndexForVisible: 0, autoscrollToTopThreshold: 40 }
|
||||
}
|
||||
initialNumToRender={12}
|
||||
windowSize={10}
|
||||
inverted
|
||||
/>
|
||||
</MessageOuterSpacingProvider>
|
||||
<ThinkingSheetProvider>
|
||||
<View style={stylesheet.container}>
|
||||
<MessageOuterSpacingProvider disableOuterSpacing>
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
data={flatListData}
|
||||
renderItem={renderStreamItem}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={{
|
||||
paddingVertical: 0,
|
||||
flexGrow: 1,
|
||||
}}
|
||||
style={stylesheet.list}
|
||||
onScroll={handleScroll}
|
||||
scrollEventThrottle={16}
|
||||
ListEmptyComponent={
|
||||
<View style={[stylesheet.emptyState, stylesheet.contentWrapper]}>
|
||||
<Text style={stylesheet.emptyStateText}>
|
||||
Start chatting with this agent...
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
ListHeaderComponent={listHeaderComponent}
|
||||
extraData={flatListExtraData}
|
||||
maintainVisibleContentPosition={
|
||||
// Disable when streaming and user is at bottom - we handle auto-scroll ourselves
|
||||
agent.status === "running" && isNearBottom
|
||||
? undefined
|
||||
: { minIndexForVisible: 0, autoscrollToTopThreshold: 40 }
|
||||
}
|
||||
initialNumToRender={12}
|
||||
windowSize={10}
|
||||
inverted
|
||||
/>
|
||||
</MessageOuterSpacingProvider>
|
||||
|
||||
{/* Scroll to bottom button */}
|
||||
{!isNearBottom && (
|
||||
<Animated.View
|
||||
style={stylesheet.scrollToBottomContainer}
|
||||
entering={scrollIndicatorFadeIn}
|
||||
exiting={scrollIndicatorFadeOut}
|
||||
>
|
||||
<View style={stylesheet.scrollToBottomInner}>
|
||||
<Pressable
|
||||
style={stylesheet.scrollToBottomButton}
|
||||
onPress={scrollToBottom}
|
||||
>
|
||||
<ChevronDown
|
||||
size={24}
|
||||
color={stylesheet.scrollToBottomIcon.color}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
{/* Scroll to bottom button */}
|
||||
{!isNearBottom && (
|
||||
<Animated.View
|
||||
style={stylesheet.scrollToBottomContainer}
|
||||
entering={scrollIndicatorFadeIn}
|
||||
exiting={scrollIndicatorFadeOut}
|
||||
>
|
||||
<View style={stylesheet.scrollToBottomInner}>
|
||||
<Pressable
|
||||
style={stylesheet.scrollToBottomButton}
|
||||
onPress={scrollToBottom}
|
||||
>
|
||||
<ChevronDown
|
||||
size={24}
|
||||
color={stylesheet.scrollToBottomIcon.color}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
</ThinkingSheetProvider>
|
||||
</ToolCallSheetProvider>
|
||||
);
|
||||
}
|
||||
|
||||
95
packages/app/src/components/agent-thought-content.tsx
Normal file
95
packages/app/src/components/agent-thought-content.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { memo, useMemo } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import Markdown from "react-native-markdown-display";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { createCompactMarkdownStyles } from "@/styles/markdown-styles";
|
||||
|
||||
export const AgentThoughtContent = memo(function AgentThoughtContent({
|
||||
message,
|
||||
}: {
|
||||
message: string;
|
||||
}) {
|
||||
const { theme } = useUnistyles();
|
||||
const markdownContent = useMemo(() => message?.trim() ?? "", [message]);
|
||||
const markdownStyles = useMemo(
|
||||
() => createCompactMarkdownStyles(theme),
|
||||
[theme]
|
||||
);
|
||||
|
||||
const markdownRules = useMemo(() => {
|
||||
return {
|
||||
text: (node: any, _children: any[], _parent: any, styles: any, inheritedStyles: any = {}) => (
|
||||
<Text key={node.key} style={[inheritedStyles, styles.text]}>
|
||||
{node.content}
|
||||
</Text>
|
||||
),
|
||||
textgroup: (node: any, children: any[], _parent: any, styles: any, inheritedStyles: any = {}) => (
|
||||
<Text key={node.key} style={[inheritedStyles, styles.textgroup]}>
|
||||
{children}
|
||||
</Text>
|
||||
),
|
||||
code_block: (node: any, _children: any[], _parent: any, styles: any, inheritedStyles: any = {}) => (
|
||||
<Text key={node.key} style={[inheritedStyles, styles.code_block]}>
|
||||
{node.content}
|
||||
</Text>
|
||||
),
|
||||
fence: (node: any, _children: any[], _parent: any, styles: any, inheritedStyles: any = {}) => (
|
||||
<Text key={node.key} style={[inheritedStyles, styles.fence]}>
|
||||
{node.content}
|
||||
</Text>
|
||||
),
|
||||
code_inline: (node: any, _children: any[], _parent: any, styles: any, inheritedStyles: any = {}) => (
|
||||
<Text key={node.key} style={[inheritedStyles, styles.code_inline]}>
|
||||
{node.content}
|
||||
</Text>
|
||||
),
|
||||
bullet_list: (node: any, children: any[], _parent: any, styles: any) => (
|
||||
<View key={node.key} style={styles.bullet_list}>
|
||||
{children}
|
||||
</View>
|
||||
),
|
||||
ordered_list: (node: any, children: any[], _parent: any, styles: any) => (
|
||||
<View key={node.key} style={styles.ordered_list}>
|
||||
{children}
|
||||
</View>
|
||||
),
|
||||
list_item: (node: any, children: any[], parent: any, styles: any) => {
|
||||
const isOrdered = parent?.type === "ordered_list";
|
||||
const index = parent?.children?.indexOf(node) ?? 0;
|
||||
const bullet = isOrdered ? `${index + 1}.` : "•";
|
||||
const iconStyle = isOrdered ? styles.ordered_list_icon : styles.bullet_list_icon;
|
||||
const contentStyle = isOrdered
|
||||
? styles.ordered_list_content
|
||||
: styles.bullet_list_content;
|
||||
|
||||
return (
|
||||
<View key={node.key} style={styles.list_item}>
|
||||
<Text style={iconStyle}>{bullet}</Text>
|
||||
<View style={[contentStyle, { flex: 1, flexShrink: 1, minWidth: 0 }]}>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!markdownContent) {
|
||||
return <Text style={styles.emptyText}>No captured thinking</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Markdown style={markdownStyles} rules={markdownRules}>
|
||||
{markdownContent}
|
||||
</Markdown>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
emptyText: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontStyle: "italic" as const,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -48,7 +48,6 @@ import {
|
||||
import { baseColors, theme } from "@/styles/theme";
|
||||
import {
|
||||
createMarkdownStyles,
|
||||
createCompactMarkdownStyles,
|
||||
} from "@/styles/markdown-styles";
|
||||
import { Colors, Fonts } from "@/constants/theme";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
@@ -57,6 +56,8 @@ import { extractPrincipalParam } from "@/utils/tool-call-parsers";
|
||||
import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
|
||||
import { resolveToolCallPreview } from "./tool-call-preview";
|
||||
import { useToolCallSheet } from "./tool-call-sheet";
|
||||
import { useThinkingSheet } from "./thinking-sheet";
|
||||
import { AgentThoughtContent } from "./agent-thought-content";
|
||||
import {
|
||||
ToolCallDetailsContent,
|
||||
useToolCallDetails,
|
||||
@@ -1194,172 +1195,41 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
);
|
||||
});
|
||||
|
||||
const agentThoughtStylesheet = StyleSheet.create((theme) => ({
|
||||
emptyText: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontStyle: "italic" as const,
|
||||
},
|
||||
}));
|
||||
|
||||
export const AgentThoughtMessage = memo(function AgentThoughtMessage({
|
||||
message,
|
||||
status = "ready",
|
||||
isLastInSequence = false,
|
||||
disableOuterSpacing,
|
||||
}: AgentThoughtMessageProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const { openThinking } = useThinkingSheet();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const markdownContent = useMemo(() => message?.trim() ?? "", [message]);
|
||||
const markdownStyles = useMemo(
|
||||
() => createCompactMarkdownStyles(theme),
|
||||
[theme]
|
||||
);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
const isMobile =
|
||||
UnistylesRuntime.breakpoint === "xs" ||
|
||||
UnistylesRuntime.breakpoint === "sm";
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (isMobile) {
|
||||
openThinking({ message, status });
|
||||
return;
|
||||
}
|
||||
setIsExpanded((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const markdownRules = useMemo(() => {
|
||||
return {
|
||||
text: (
|
||||
node: any,
|
||||
_children: ReactNode[],
|
||||
_parent: any,
|
||||
styles: any,
|
||||
inheritedStyles: any = {}
|
||||
) => (
|
||||
<Text key={node.key} style={[inheritedStyles, styles.text]}>
|
||||
{node.content}
|
||||
</Text>
|
||||
),
|
||||
textgroup: (
|
||||
node: any,
|
||||
children: ReactNode[],
|
||||
_parent: any,
|
||||
styles: any,
|
||||
inheritedStyles: any = {}
|
||||
) => (
|
||||
<Text
|
||||
key={node.key}
|
||||
style={[inheritedStyles, styles.textgroup]}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
),
|
||||
code_block: (
|
||||
node: any,
|
||||
_children: ReactNode[],
|
||||
_parent: any,
|
||||
styles: any,
|
||||
inheritedStyles: any = {}
|
||||
) => (
|
||||
<Text
|
||||
key={node.key}
|
||||
style={[inheritedStyles, styles.code_block]}
|
||||
>
|
||||
{node.content}
|
||||
</Text>
|
||||
),
|
||||
fence: (
|
||||
node: any,
|
||||
_children: ReactNode[],
|
||||
_parent: any,
|
||||
styles: any,
|
||||
inheritedStyles: any = {}
|
||||
) => (
|
||||
<Text key={node.key} style={[inheritedStyles, styles.fence]}>
|
||||
{node.content}
|
||||
</Text>
|
||||
),
|
||||
code_inline: (
|
||||
node: any,
|
||||
_children: ReactNode[],
|
||||
_parent: any,
|
||||
styles: any,
|
||||
inheritedStyles: any = {}
|
||||
) => (
|
||||
<Text
|
||||
key={node.key}
|
||||
style={[inheritedStyles, styles.code_inline]}
|
||||
>
|
||||
{node.content}
|
||||
</Text>
|
||||
),
|
||||
bullet_list: (
|
||||
node: any,
|
||||
children: ReactNode[],
|
||||
_parent: any,
|
||||
styles: any
|
||||
) => (
|
||||
<View key={node.key} style={styles.bullet_list}>
|
||||
{children}
|
||||
</View>
|
||||
),
|
||||
ordered_list: (
|
||||
node: any,
|
||||
children: ReactNode[],
|
||||
_parent: any,
|
||||
styles: any
|
||||
) => (
|
||||
<View key={node.key} style={styles.ordered_list}>
|
||||
{children}
|
||||
</View>
|
||||
),
|
||||
list_item: (
|
||||
node: any,
|
||||
children: ReactNode[],
|
||||
parent: any,
|
||||
styles: any
|
||||
) => {
|
||||
const isOrdered = parent?.type === "ordered_list";
|
||||
const index = parent?.children?.indexOf(node) ?? 0;
|
||||
const bullet = isOrdered ? `${index + 1}.` : "•";
|
||||
const iconStyle = isOrdered
|
||||
? styles.ordered_list_icon
|
||||
: styles.bullet_list_icon;
|
||||
const contentStyle = isOrdered
|
||||
? styles.ordered_list_content
|
||||
: styles.bullet_list_content;
|
||||
|
||||
return (
|
||||
<View key={node.key} style={styles.list_item}>
|
||||
<Text style={iconStyle}>{bullet}</Text>
|
||||
<View
|
||||
style={[contentStyle, { flex: 1, flexShrink: 1, minWidth: 0 }]}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
}, [isMobile, openThinking, message, status]);
|
||||
|
||||
const renderDetails = useCallback(() => {
|
||||
if (!markdownContent) {
|
||||
return (
|
||||
<Text style={agentThoughtStylesheet.emptyText}>
|
||||
No captured thinking
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Markdown style={markdownStyles} rules={markdownRules}>
|
||||
{markdownContent}
|
||||
</Markdown>
|
||||
);
|
||||
}, [markdownContent, markdownRules, markdownStyles]);
|
||||
return <AgentThoughtContent message={message} />;
|
||||
}, [message]);
|
||||
|
||||
return (
|
||||
<ExpandableBadge
|
||||
label="Thinking"
|
||||
icon={status === "ready" ? Brain : undefined}
|
||||
isExpanded={isExpanded}
|
||||
onToggle={toggle}
|
||||
renderDetails={renderDetails}
|
||||
isExpanded={!isMobile && isExpanded}
|
||||
onToggle={handleToggle}
|
||||
renderDetails={isMobile ? () => null : renderDetails}
|
||||
isLoading={status !== "ready"}
|
||||
isLastInSequence={isLastInSequence}
|
||||
style={isLastInSequence ? undefined : { marginBottom: theme.spacing[1] }}
|
||||
disableOuterSpacing={disableOuterSpacing}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ToolCall,
|
||||
} from "@/components/message";
|
||||
import { ToolCallSheetProvider } from "@/components/tool-call-sheet";
|
||||
import { ThinkingSheetProvider } from "@/components/thinking-sheet";
|
||||
import type { MessageEntry } from "@/contexts/session-context";
|
||||
|
||||
interface OrchestratorMessagesViewProps {
|
||||
@@ -21,86 +22,88 @@ export const OrchestratorMessagesView = forwardRef<ScrollView, OrchestratorMessa
|
||||
|
||||
return (
|
||||
<ToolCallSheetProvider>
|
||||
<ScrollView
|
||||
ref={ref}
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="on-drag"
|
||||
>
|
||||
{messages.map((msg) => {
|
||||
if (msg.type === "user") {
|
||||
return (
|
||||
<UserMessage
|
||||
key={msg.id}
|
||||
message={msg.message}
|
||||
timestamp={msg.timestamp}
|
||||
/>
|
||||
);
|
||||
}
|
||||
<ThinkingSheetProvider>
|
||||
<ScrollView
|
||||
ref={ref}
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="on-drag"
|
||||
>
|
||||
{messages.map((msg) => {
|
||||
if (msg.type === "user") {
|
||||
return (
|
||||
<UserMessage
|
||||
key={msg.id}
|
||||
message={msg.message}
|
||||
timestamp={msg.timestamp}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.type === "assistant") {
|
||||
return (
|
||||
<AssistantMessage
|
||||
key={msg.id}
|
||||
message={msg.message}
|
||||
timestamp={msg.timestamp}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (msg.type === "assistant") {
|
||||
return (
|
||||
<AssistantMessage
|
||||
key={msg.id}
|
||||
message={msg.message}
|
||||
timestamp={msg.timestamp}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.type === "activity") {
|
||||
return (
|
||||
<ActivityLog
|
||||
key={msg.id}
|
||||
type={msg.activityType}
|
||||
message={msg.message}
|
||||
timestamp={msg.timestamp}
|
||||
metadata={msg.metadata}
|
||||
onArtifactClick={onArtifactClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (msg.type === "activity") {
|
||||
return (
|
||||
<ActivityLog
|
||||
key={msg.id}
|
||||
type={msg.activityType}
|
||||
message={msg.message}
|
||||
timestamp={msg.timestamp}
|
||||
metadata={msg.metadata}
|
||||
onArtifactClick={onArtifactClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.type === "artifact") {
|
||||
return (
|
||||
<ActivityLog
|
||||
key={msg.id}
|
||||
type="artifact"
|
||||
message=""
|
||||
timestamp={msg.timestamp}
|
||||
artifactId={msg.artifactId}
|
||||
artifactType={msg.artifactType}
|
||||
title={msg.title}
|
||||
onArtifactClick={onArtifactClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (msg.type === "artifact") {
|
||||
return (
|
||||
<ActivityLog
|
||||
key={msg.id}
|
||||
type="artifact"
|
||||
message=""
|
||||
timestamp={msg.timestamp}
|
||||
artifactId={msg.artifactId}
|
||||
artifactType={msg.artifactType}
|
||||
title={msg.title}
|
||||
onArtifactClick={onArtifactClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.type === "tool_call") {
|
||||
return (
|
||||
<ToolCall
|
||||
key={msg.id}
|
||||
toolName={msg.toolName}
|
||||
args={msg.args}
|
||||
result={msg.result}
|
||||
error={msg.error}
|
||||
status={msg.status}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (msg.type === "tool_call") {
|
||||
return (
|
||||
<ToolCall
|
||||
key={msg.id}
|
||||
toolName={msg.toolName}
|
||||
args={msg.args}
|
||||
result={msg.result}
|
||||
error={msg.error}
|
||||
status={msg.status}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
return null;
|
||||
})}
|
||||
|
||||
{/* Streaming assistant message */}
|
||||
{currentAssistantMessage && (
|
||||
<AssistantMessage
|
||||
message={currentAssistantMessage}
|
||||
timestamp={Date.now()}
|
||||
/>
|
||||
)}
|
||||
</ScrollView>
|
||||
{/* Streaming assistant message */}
|
||||
{currentAssistantMessage && (
|
||||
<AssistantMessage
|
||||
message={currentAssistantMessage}
|
||||
timestamp={Date.now()}
|
||||
/>
|
||||
)}
|
||||
</ScrollView>
|
||||
</ThinkingSheetProvider>
|
||||
</ToolCallSheetProvider>
|
||||
);
|
||||
}
|
||||
|
||||
182
packages/app/src/components/thinking-sheet.tsx
Normal file
182
packages/app/src/components/thinking-sheet.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useRef,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import { View, Text, Pressable } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import {
|
||||
BottomSheetModal,
|
||||
BottomSheetScrollView,
|
||||
BottomSheetBackdrop,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { Brain, X } from "lucide-react-native";
|
||||
import type { ThoughtStatus } from "@/types/stream";
|
||||
import { AgentThoughtContent } from "./agent-thought-content";
|
||||
|
||||
export interface ThinkingSheetData {
|
||||
message: string;
|
||||
status?: ThoughtStatus;
|
||||
}
|
||||
|
||||
interface ThinkingSheetContextValue {
|
||||
openThinking: (data: ThinkingSheetData) => void;
|
||||
closeThinking: () => void;
|
||||
}
|
||||
|
||||
const ThinkingSheetContext = createContext<ThinkingSheetContextValue | null>(null);
|
||||
|
||||
export function useThinkingSheet(): ThinkingSheetContextValue {
|
||||
const context = useContext(ThinkingSheetContext);
|
||||
if (!context) {
|
||||
throw new Error("useThinkingSheet must be used within a ThinkingSheetProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function ThinkingSheetProvider({ children }: { children: ReactNode }) {
|
||||
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
||||
const [sheetData, setSheetData] = React.useState<ThinkingSheetData | null>(null);
|
||||
|
||||
const snapPoints = useMemo(() => ["60%", "95%"], []);
|
||||
|
||||
const openThinking = useCallback((data: ThinkingSheetData) => {
|
||||
setSheetData(data);
|
||||
bottomSheetRef.current?.present();
|
||||
}, []);
|
||||
|
||||
const closeThinking = useCallback(() => {
|
||||
bottomSheetRef.current?.dismiss();
|
||||
}, []);
|
||||
|
||||
const handleSheetChange = useCallback((index: number) => {
|
||||
if (index === -1) {
|
||||
setSheetData(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: React.ComponentProps<typeof BottomSheetBackdrop>) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
opacity={0.5}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({ openThinking, closeThinking }),
|
||||
[openThinking, closeThinking]
|
||||
);
|
||||
|
||||
return (
|
||||
<ThinkingSheetContext.Provider value={contextValue}>
|
||||
{children}
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetRef}
|
||||
snapPoints={snapPoints}
|
||||
index={0}
|
||||
enableDynamicSizing={false}
|
||||
onChange={handleSheetChange}
|
||||
backdropComponent={renderBackdrop}
|
||||
enablePanDownToClose
|
||||
backgroundStyle={styles.sheetBackground}
|
||||
handleIndicatorStyle={styles.handleIndicator}
|
||||
>
|
||||
{sheetData && (
|
||||
<ThinkingSheetContent data={sheetData} onClose={closeThinking} />
|
||||
)}
|
||||
</BottomSheetModal>
|
||||
</ThinkingSheetContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function ThinkingSheetContent({
|
||||
data,
|
||||
onClose,
|
||||
}: {
|
||||
data: ThinkingSheetData;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { message } = data;
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<Brain size={20} color={styles.headerIcon.color} />
|
||||
<Text style={styles.headerTitle} numberOfLines={1}>
|
||||
Thinking
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable onPress={onClose} style={styles.closeButton}>
|
||||
<X size={20} color={styles.closeIcon.color} />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<BottomSheetScrollView
|
||||
style={styles.content}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
>
|
||||
<AgentThoughtContent message={message} />
|
||||
</BottomSheetScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
sheetBackground: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
handleIndicator: {
|
||||
backgroundColor: theme.colors.palette.zinc[600],
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingVertical: theme.spacing[3],
|
||||
borderBottomWidth: theme.borderWidth[1],
|
||||
borderBottomColor: theme.colors.border,
|
||||
},
|
||||
headerLeft: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
flex: 1,
|
||||
},
|
||||
headerIcon: {
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: theme.fontSize.lg,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
color: theme.colors.foreground,
|
||||
flex: 1,
|
||||
},
|
||||
closeButton: {
|
||||
padding: theme.spacing[2],
|
||||
},
|
||||
closeIcon: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
contentContainer: {
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingTop: theme.spacing[4],
|
||||
paddingBottom: theme.spacing[8],
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user