diff --git a/app.config.ts b/app.config.ts index a34dafad..20ffbb80 100644 --- a/app.config.ts +++ b/app.config.ts @@ -19,7 +19,8 @@ module.exports = ({ config }: ConfigContext): Partial => { ...config, plugins: [ ...existingPlugins, - 'expo-sqlite' + 'expo-localization', + 'expo-sqlite', ], extra: { ...config.extra, diff --git a/app.json b/app.json index 89ebb93f..7483c610 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "name": "Onyx", "slug": "onyx", "scheme": "onyx", - "version": "0.0.5", + "version": "0.0.6", "orientation": "portrait", "userInterfaceStyle": "automatic", "icon": "./assets/images/app-icon-all.png", diff --git a/app/app.tsx b/app/app.tsx index d86d2999..018b7ed1 100644 --- a/app/app.tsx +++ b/app/app.tsx @@ -4,39 +4,41 @@ if (__DEV__) { import "react-native-gesture-handler" import "@/utils/ignore-warnings" -import "@/utils/crypto-polyfill" -import "text-encoding-polyfill" -import { Buffer } from "buffer" +import "@/utils/polyfills" import { useFonts } from "expo-font" -import { StatusBar } from "expo-status-bar" import * as React from "react" -import { ActivityIndicator, Alert, AppRegistry, View, ViewStyle } from "react-native" +import { ActivityIndicator, AppRegistry, View } from "react-native" +import { KeyboardProvider } from "react-native-keyboard-controller" import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context" -import { Canvas } from "@/canvas" import { customFontsToLoad } from "@/theme/typography" -import { Chat } from "./chat/Chat" import Config from "./config" import { useAutoUpdate } from "./hooks/useAutoUpdate" import { useInitialRootStore } from "./models" -import { OnyxLayout } from "./onyx/OnyxLayout" +import { AppNavigator, useNavigationPersistence } from "./navigators" import { ErrorBoundary } from "./screens/ErrorScreen/ErrorBoundary" - -global.Buffer = Buffer +import * as storage from "./utils/storage" interface AppProps { hideSplashScreen: () => Promise } -function AppContents(props: AppProps) { +export const NAVIGATION_PERSISTENCE_KEY = "NAVIGATION_STATE" + +function App(props: AppProps) { useAutoUpdate() const { hideSplashScreen } = props + const { + initialNavigationState, + onNavigationStateChange, + isRestored: isNavigationStateRestored, + } = useNavigationPersistence(storage, NAVIGATION_PERSISTENCE_KEY) const [loaded] = useFonts(customFontsToLoad) const { rehydrated } = useInitialRootStore(() => { // This runs after the root store has been initialized and rehydrated. setTimeout(hideSplashScreen, 500) }) - if (!loaded || !rehydrated) { + if (!loaded || !rehydrated || !isNavigationStateRestored) { return ( @@ -47,36 +49,17 @@ function AppContents(props: AppProps) { return ( - - - - - - - + + + ) } -function App(props: AppProps) { - return -} - -const $container: ViewStyle = { - flex: 1, - backgroundColor: "#000", -} - -const $canvasContainer: ViewStyle = { - position: "absolute", - top: 0, - left: 0, - right: 0, - bottom: 0, - zIndex: 0, -} - AppRegistry.registerComponent("main", () => App) export default App diff --git a/app/chat/Chat.tsx b/app/chat/Chat.tsx index 9fe179d2..beca19ad 100644 --- a/app/chat/Chat.tsx +++ b/app/chat/Chat.tsx @@ -1,53 +1,43 @@ -import { useState } from "react" -import { Dimensions, TouchableOpacity, View } from "react-native" -import { Drawer } from "react-native-drawer-layout" -import { OnyxLayout } from "@/onyx/OnyxLayout" -import { useSafeAreaInsetsStyle } from "@/utils/useSafeAreaInsetsStyle" -import { Feather } from "@expo/vector-icons" -import { ChatDrawerContent } from "./ChatDrawerContent" +import { observer } from "mobx-react-lite" +import { Platform, View } from "react-native" +import { KeyboardAwareScrollView } from "react-native-keyboard-controller" +import { Header } from "@/components" +import { useChat } from "@/hooks/useChat" +import { navigate } from "@/navigators" +import { ChatBar } from "./ChatBar" +import { ChatOverlay } from "./ChatOverlay" -export const Chat = () => { - const [open, setOpen] = useState(false) - const $drawerInsets = useSafeAreaInsetsStyle(["top"]) +interface ChatProps { + drawerOpen: boolean + setDrawerOpen: (open: boolean) => void +} + +export const Chat = observer(({ drawerOpen, setDrawerOpen }: ChatProps) => { + const { handleSendMessage, isLoading, messages } = useChat() return ( - setOpen(true)} - onClose={() => setOpen(false)} - drawerType="slide" - renderDrawerContent={() => ( - - )} - > - - setOpen((prevOpen) => !prevOpen)} - style={{ - position: "absolute", - top: 55, - left: 15, - zIndex: 900, - backgroundColor: "rgba(32, 32, 32, 0.8)", - padding: 8, - borderRadius: 4, - }} + + +
setDrawerOpen(!drawerOpen)} + rightIcon="settings" + onRightPress={() => navigate("Settings")} + /> + - - - - - + + + + + - + ) -} +}) diff --git a/app/chat/ChatBar.tsx b/app/chat/ChatBar.tsx new file mode 100644 index 00000000..7fac6997 --- /dev/null +++ b/app/chat/ChatBar.tsx @@ -0,0 +1,159 @@ +import { useRef, useState } from "react" +import { Keyboard, Pressable, TextInput, View } from "react-native" +import { ThinkingAnimation } from "@/components/ThinkingAnimation" +import { useKeyboard } from "@/hooks/useKeyboard" +import { useVoiceRecording } from "@/hooks/useVoiceRecording" +import { colorsDark as colors } from "@/theme" +import { AntDesign, FontAwesome } from "@expo/vector-icons" +import { typography } from "../theme/typography" + +interface ChatBarProps { + handleSendMessage: (message: string) => void +} + +export const ChatBar = ({ handleSendMessage }: ChatBarProps) => { + const [height, setHeight] = useState(24) + const [text, setText] = useState("") + const sendImmediatelyRef = useRef(false) + const { isOpened: expanded, show, ref } = useKeyboard() + const { isRecording, isProcessing, startRecording, stopRecording } = useVoiceRecording( + (transcription) => { + const trimmedTranscription = transcription.trim() + if (sendImmediatelyRef.current) { + // Send all accumulated text plus new transcription + const existingText = text.trim() + const fullMessage = existingText + ? `${existingText} ${trimmedTranscription}` + : trimmedTranscription + handleSendMessage(fullMessage) + setText("") + sendImmediatelyRef.current = false + } else { + setText((prev) => { + if (!prev.trim()) return trimmedTranscription + return `${prev.trim()} ${trimmedTranscription}` + }) + } + }, + ) + + const updateSize = (event) => { + const newHeight = Math.min(event.nativeEvent.contentSize.height, 240) + setHeight(newHeight) + } + + const handleMicPress = async (e) => { + e.stopPropagation() + if (isRecording) { + sendImmediatelyRef.current = false + await stopRecording() + } else { + await startRecording() + } + } + + const handleSendPress = async (e) => { + e.stopPropagation() + if (isRecording) { + sendImmediatelyRef.current = true + await stopRecording() + } else if (text.trim()) { + handleSendMessage(text) + setText("") + Keyboard.dismiss() + } + } + + const handleBarPress = () => { + if (!expanded) { + show() + } + } + + return ( + + + + + + + + + {isProcessing && ( + + + + )} + + + + + + + + ) +} diff --git a/app/chat/ChatDrawerContainer.tsx b/app/chat/ChatDrawerContainer.tsx new file mode 100644 index 00000000..b99b974e --- /dev/null +++ b/app/chat/ChatDrawerContainer.tsx @@ -0,0 +1,34 @@ +import { useState } from "react" +import { Platform } from "react-native" +import { Drawer } from "react-native-drawer-layout" +import { Screen } from "@/components/Screen" +import { $styles } from "@/theme" +import { useSafeAreaInsetsStyle } from "@/utils/useSafeAreaInsetsStyle" +import { Chat } from "./Chat" +import { ChatDrawerContent } from "./ChatDrawerContent" + +export const ChatDrawerContainer = () => { + const [open, setOpen] = useState(false) + const $drawerInsets = useSafeAreaInsetsStyle(["top"]) + + return ( + setOpen(true)} + onClose={() => setOpen(false)} + drawerType="slide" + renderDrawerContent={() => ( + + )} + > + + + + + ) +} \ No newline at end of file diff --git a/app/chat/ChatDrawerContent.tsx b/app/chat/ChatDrawerContent.tsx index 9ae59aa1..f95507bc 100644 --- a/app/chat/ChatDrawerContent.tsx +++ b/app/chat/ChatDrawerContent.tsx @@ -1,9 +1,9 @@ -import { Text, View, TouchableOpacity, ScrollView } from "react-native" -import { colors, typography } from "@/theme" -import { MaterialCommunityIcons } from "@expo/vector-icons" -import { useStores } from "@/models" import { observer } from "mobx-react-lite" import { useEffect } from "react" +import { ScrollView, Text, TouchableOpacity, View } from "react-native" +import { useStores } from "@/models" +import { colorsDark as colors, typography } from "@/theme" +import { MaterialCommunityIcons } from "@expo/vector-icons" type Props = { drawerInsets: any // replace any with the correct type @@ -34,9 +34,7 @@ export const ChatDrawerContent = observer(({ drawerInsets, setOpen }: Props) => if (!messages || messages.length === 0) { return "New Chat" } - const lastUserMessage = messages - .filter(msg => msg.role === "user") - .pop() + const lastUserMessage = messages.filter((msg) => msg.role === "user").pop() if (!lastUserMessage) { return "New Chat" } @@ -49,8 +47,8 @@ export const ChatDrawerContent = observer(({ drawerInsets, setOpen }: Props) => // Sort chats by creation time (using first message's timestamp or chat ID timestamp) const sortedChats = [...chatStore.allChats].sort((a, b) => { - const aTime = a.messages[0]?.createdAt || parseInt(a.id.split('_')[1]) || 0 - const bTime = b.messages[0]?.createdAt || parseInt(b.id.split('_')[1]) || 0 + const aTime = a.messages[0]?.createdAt || parseInt(a.id.split("_")[1]) || 0 + const bTime = b.messages[0]?.createdAt || parseInt(b.id.split("_")[1]) || 0 return bTime - aTime // Reverse chronological order }) @@ -72,7 +70,7 @@ export const ChatDrawerContent = observer(({ drawerInsets, setOpen }: Props) => borderBottomColor: colors.border, }} > - padding: 16, borderBottomWidth: 1, borderBottomColor: colors.border, - backgroundColor: chatStore.currentConversationId === chat.id - ? colors.palette.neutral200 - : "transparent", + backgroundColor: + chatStore.currentConversationId === chat.id + ? colors.palette.neutral200 + : "transparent", }} > - > {getChatPreview(chat.messages)} - - {new Date(chat.messages[0]?.createdAt || parseInt(chat.id.split('_')[1]) || Date.now()).toLocaleDateString()} + {new Date( + chat.messages[0]?.createdAt || parseInt(chat.id.split("_")[1]) || Date.now(), + ).toLocaleDateString()} ))} ) -}) \ No newline at end of file +}) diff --git a/app/onyx/ChatOverlay.tsx b/app/chat/ChatOverlay.tsx similarity index 72% rename from app/onyx/ChatOverlay.tsx rename to app/chat/ChatOverlay.tsx index 877c47c0..74d9b629 100644 --- a/app/onyx/ChatOverlay.tsx +++ b/app/chat/ChatOverlay.tsx @@ -1,6 +1,7 @@ import { observer } from "mobx-react-lite" import React, { useEffect, useRef } from "react" -import { Image, ScrollView, TouchableOpacity, View } from "react-native" +import { ScrollView, TouchableOpacity, View } from "react-native" +import { ThinkingAnimation } from "@/components/ThinkingAnimation" import { Message } from "@ai-sdk/react" import Clipboard from "@react-native-clipboard/clipboard" import { MessageContent } from "./markdown/MessageContent" @@ -18,17 +19,17 @@ export const ChatOverlay = observer(({ messages, isLoading, error }: ChatOverlay useEffect(() => { // Scroll to bottom whenever messages change scrollViewRef.current?.scrollToEnd({ animated: true }) - }, [messages]) + }, [messages, isLoading]) const copyToClipboard = (content: string) => { Clipboard.setString(content) } return ( - + {messages.map((message: Message) => ( @@ -42,21 +43,8 @@ export const ChatOverlay = observer(({ messages, isLoading, error }: ChatOverlay ))} + {isLoading && } - - {isLoading && ( - - )} ) -}) +}) \ No newline at end of file diff --git a/app/onyx/markdown/MessageContent.tsx b/app/chat/markdown/MessageContent.tsx similarity index 91% rename from app/onyx/markdown/MessageContent.tsx rename to app/chat/markdown/MessageContent.tsx index 815ae185..882254b6 100644 --- a/app/onyx/markdown/MessageContent.tsx +++ b/app/chat/markdown/MessageContent.tsx @@ -1,7 +1,7 @@ import React from "react" import { Linking, StyleSheet, View } from "react-native" import Markdown from "react-native-markdown-display" -import { colors } from "@/theme" +import { colorsDark as colors } from "@/theme" import { Message } from "@ai-sdk/react" import { markdownStyles } from "./styles" import { ToolInvocation } from "./ToolInvocation" @@ -28,7 +28,8 @@ export function MessageContent({ message }: MessageContentProps) { } const isUserMessage = message.role === "user" - const hasToolInvocations = !isUserMessage && Array.isArray(message.toolInvocations) && message.toolInvocations.length > 0 + const hasToolInvocations = + !isUserMessage && Array.isArray(message.toolInvocations) && message.toolInvocations.length > 0 const hasContent = message.content && message.content.trim() !== "" return ( @@ -73,4 +74,4 @@ const styles = StyleSheet.create({ contentAfterTools: { marginTop: 8, }, -}) \ No newline at end of file +}) diff --git a/app/onyx/markdown/ToolInvocation.tsx b/app/chat/markdown/ToolInvocation.tsx similarity index 98% rename from app/onyx/markdown/ToolInvocation.tsx rename to app/chat/markdown/ToolInvocation.tsx index a3789319..e5c51917 100644 --- a/app/onyx/markdown/ToolInvocation.tsx +++ b/app/chat/markdown/ToolInvocation.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react" import { Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native" -import { colors, typography } from "@/theme" +import { colorsDark as colors, typography } from "@/theme" interface JSONValue { [key: string]: any @@ -154,7 +154,7 @@ export function ToolInvocation({ toolInvocation }: { toolInvocation: ToolInvocat const styles = StyleSheet.create({ card: { - backgroundColor: colors.background, + backgroundColor: colors.backgroundSecondary, borderRadius: 8, marginBottom: 8, borderWidth: 1, diff --git a/app/onyx/markdown/index.ts b/app/chat/markdown/index.ts similarity index 100% rename from app/onyx/markdown/index.ts rename to app/chat/markdown/index.ts diff --git a/app/onyx/markdown/styles.ts b/app/chat/markdown/styles.ts similarity index 97% rename from app/onyx/markdown/styles.ts rename to app/chat/markdown/styles.ts index a679aa43..28e41ae6 100644 --- a/app/onyx/markdown/styles.ts +++ b/app/chat/markdown/styles.ts @@ -1,5 +1,5 @@ import { StyleSheet } from "react-native" -import { colors, typography } from "@/theme" +import { colorsDark as colors, typography } from "@/theme" export const markdownStyles = StyleSheet.create({ body: { diff --git a/app/chat/styles.ts b/app/chat/styles.ts new file mode 100644 index 00000000..155a7c4f --- /dev/null +++ b/app/chat/styles.ts @@ -0,0 +1,75 @@ +import { StyleSheet } from "react-native" +import { colors } from "@/theme/colorsDark" +import { typography } from "@/theme/typography" + +export const styles = StyleSheet.create({ + // Modal base styles + modalContainer: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.85)", + }, + modalHeader: { + flexDirection: "row", + justifyContent: "space-between", + paddingHorizontal: 20, + paddingTop: 60, + paddingBottom: 15, + }, + buttonText: { + fontSize: 17, + fontFamily: typography.primary.normal, + }, + cancelText: { + color: "#666", + }, + sendText: { + color: "#fff", + }, + disabledText: { + color: "#666", + }, + input: { + color: "#fff", + fontSize: 17, + paddingHorizontal: 20, + paddingTop: 0, + fontFamily: typography.primary.normal, + }, + + // Chat Overlay styles + chatOverlay: { + position: "absolute", + top: 40, + left: 0, + right: 0, + bottom: 110, + padding: 10, + zIndex: 5, + borderBottomWidth: 1, + borderBottomColor: "rgba(255,255,255,0.1)", + }, + messageList: { + flex: 1, + }, + message: { + marginBottom: 12, + }, + messageText: { + color: "#fff", + fontSize: 15, + fontFamily: typography.primary.normal, + }, + + // Error styles + errorContainer: { + backgroundColor: colors.errorBackground, + padding: 12, + borderRadius: 8, + marginBottom: 16, + }, + errorText: { + color: colors.error, + fontSize: 14, + fontFamily: typography.primary.normal, + }, +}) diff --git a/app/components/AutoImage.tsx b/app/components/AutoImage.tsx new file mode 100644 index 00000000..44afcf35 --- /dev/null +++ b/app/components/AutoImage.tsx @@ -0,0 +1,74 @@ +import { useLayoutEffect, useState } from "react" +import { Image, ImageProps, ImageURISource, Platform } from "react-native" + +export interface AutoImageProps extends ImageProps { + /** + * How wide should the image be? + */ + maxWidth?: number + /** + * How tall should the image be? + */ + maxHeight?: number +} + +/** + * A hook that will return the scaled dimensions of an image based on the + * provided dimensions' aspect ratio. If no desired dimensions are provided, + * it will return the original dimensions of the remote image. + * + * How is this different from `resizeMode: 'contain'`? Firstly, you can + * specify only one side's size (not both). Secondly, the image will scale to fit + * the desired dimensions instead of just being contained within its image-container. + * @param {number} remoteUri - The URI of the remote image. + * @param {number} dimensions - The desired dimensions of the image. If not provided, the original dimensions will be returned. + * @returns {[number, number]} - The scaled dimensions of the image. + */ +export function useAutoImage( + remoteUri: string, + dimensions?: [maxWidth?: number, maxHeight?: number], +): [width: number, height: number] { + const [[remoteWidth, remoteHeight], setRemoteImageDimensions] = useState([0, 0]) + const remoteAspectRatio = remoteWidth / remoteHeight + const [maxWidth, maxHeight] = dimensions ?? [] + + useLayoutEffect(() => { + if (!remoteUri) return + + Image.getSize(remoteUri, (w, h) => setRemoteImageDimensions([w, h])) + }, [remoteUri]) + + if (Number.isNaN(remoteAspectRatio)) return [0, 0] + + if (maxWidth && maxHeight) { + const aspectRatio = Math.min(maxWidth / remoteWidth, maxHeight / remoteHeight) + return [remoteWidth * aspectRatio, remoteHeight * aspectRatio] + } else if (maxWidth) { + return [maxWidth, maxWidth / remoteAspectRatio] + } else if (maxHeight) { + return [maxHeight * remoteAspectRatio, maxHeight] + } else { + return [remoteWidth, remoteHeight] + } +} + +/** + * An Image component that automatically sizes a remote or data-uri image. + * @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/components/AutoImage/} + * @param {AutoImageProps} props - The props for the `AutoImage` component. + * @returns {JSX.Element} The rendered `AutoImage` component. + */ +export function AutoImage(props: AutoImageProps) { + const { maxWidth, maxHeight, ...ImageProps } = props + const source = props.source as ImageURISource + + const [width, height] = useAutoImage( + Platform.select({ + web: (source?.uri as string) ?? (source as string), + default: source?.uri as string, + }), + [maxWidth, maxHeight], + ) + + return +} diff --git a/app/components/Header.tsx b/app/components/Header.tsx new file mode 100644 index 00000000..9acc3e8a --- /dev/null +++ b/app/components/Header.tsx @@ -0,0 +1,326 @@ +import { ReactElement } from "react" +import { + StyleProp, + TextStyle, + TouchableOpacity, + TouchableOpacityProps, + View, + ViewStyle, +} from "react-native" +import { useAppTheme } from "@/utils/useAppTheme" +import { isRTL, translate } from "../i18n" +import { $styles } from "../theme" +import { ExtendedEdge, useSafeAreaInsetsStyle } from "../utils/useSafeAreaInsetsStyle" +import { Icon, IconTypes } from "./Icon" +import { Text, TextProps } from "./Text" + +import type { ThemedStyle } from "@/theme" + +export interface HeaderProps { + /** + * The layout of the title relative to the action components. + * - `center` will force the title to always be centered relative to the header. If the title or the action buttons are too long, the title will be cut off. + * - `flex` will attempt to center the title relative to the action buttons. If the action buttons are different widths, the title will be off-center relative to the header. + */ + titleMode?: "center" | "flex" + /** + * Optional title style override. + */ + titleStyle?: StyleProp + /** + * Optional outer title container style override. + */ + titleContainerStyle?: StyleProp + /** + * Optional inner header wrapper style override. + */ + style?: StyleProp + /** + * Optional outer header container style override. + */ + containerStyle?: StyleProp + /** + * Background color + */ + backgroundColor?: string + /** + * Title text to display if not using `tx` or nested components. + */ + title?: TextProps["text"] + /** + * Title text which is looked up via i18n. + */ + titleTx?: TextProps["tx"] + /** + * Optional options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + titleTxOptions?: TextProps["txOptions"] + /** + * Icon that should appear on the left. + * Can be used with `onLeftPress`. + */ + leftIcon?: IconTypes + /** + * An optional tint color for the left icon + */ + leftIconColor?: string + /** + * Left action text to display if not using `leftTx`. + * Can be used with `onLeftPress`. Overrides `leftIcon`. + */ + leftText?: TextProps["text"] + /** + * Left action text text which is looked up via i18n. + * Can be used with `onLeftPress`. Overrides `leftIcon`. + */ + leftTx?: TextProps["tx"] + /** + * Left action custom ReactElement if the built in action props don't suffice. + * Overrides `leftIcon`, `leftTx` and `leftText`. + */ + LeftActionComponent?: ReactElement + /** + * Optional options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + leftTxOptions?: TextProps["txOptions"] + /** + * What happens when you press the left icon or text action. + */ + onLeftPress?: TouchableOpacityProps["onPress"] + /** + * Icon that should appear on the right. + * Can be used with `onRightPress`. + */ + rightIcon?: IconTypes + /** + * An optional tint color for the right icon + */ + rightIconColor?: string + /** + * Right action text to display if not using `rightTx`. + * Can be used with `onRightPress`. Overrides `rightIcon`. + */ + rightText?: TextProps["text"] + /** + * Right action text text which is looked up via i18n. + * Can be used with `onRightPress`. Overrides `rightIcon`. + */ + rightTx?: TextProps["tx"] + /** + * Right action custom ReactElement if the built in action props don't suffice. + * Overrides `rightIcon`, `rightTx` and `rightText`. + */ + RightActionComponent?: ReactElement + /** + * Optional options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + rightTxOptions?: TextProps["txOptions"] + /** + * What happens when you press the right icon or text action. + */ + onRightPress?: TouchableOpacityProps["onPress"] + /** + * Override the default edges for the safe area. + */ + safeAreaEdges?: ExtendedEdge[] +} + +interface HeaderActionProps { + backgroundColor?: string + icon?: IconTypes + iconColor?: string + text?: TextProps["text"] + tx?: TextProps["tx"] + txOptions?: TextProps["txOptions"] + onPress?: TouchableOpacityProps["onPress"] + ActionComponent?: ReactElement +} + +/** + * Header that appears on many screens. Will hold navigation buttons and screen title. + * The Header is meant to be used with the `screenOptions.header` option on navigators, routes, or screen components via `navigation.setOptions({ header })`. + * @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/components/Header/} + * @param {HeaderProps} props - The props for the `Header` component. + * @returns {JSX.Element} The rendered `Header` component. + */ +export function Header(props: HeaderProps) { + const { + theme: { colors }, + themed, + } = useAppTheme() + const { + backgroundColor = colors.background, + LeftActionComponent, + leftIcon, + leftIconColor, + leftText, + leftTx, + leftTxOptions, + onLeftPress, + onRightPress, + RightActionComponent, + rightIcon, + rightIconColor, + rightText, + rightTx, + rightTxOptions, + safeAreaEdges = ["top"], + title, + titleMode = "center", + titleTx, + titleTxOptions, + titleContainerStyle: $titleContainerStyleOverride, + style: $styleOverride, + titleStyle: $titleStyleOverride, + containerStyle: $containerStyleOverride, + } = props + + const $containerInsets = useSafeAreaInsetsStyle(safeAreaEdges) + + const titleContent = titleTx ? translate(titleTx, titleTxOptions) : title + + return ( + + + + + {!!titleContent && ( + + + + )} + + + + + ) +} + +/** + * @param {HeaderActionProps} props - The props for the `HeaderAction` component. + * @returns {JSX.Element} The rendered `HeaderAction` component. + */ +function HeaderAction(props: HeaderActionProps) { + const { backgroundColor, icon, text, tx, txOptions, onPress, ActionComponent, iconColor } = props + const { themed } = useAppTheme() + + const content = tx ? translate(tx, txOptions) : text + + if (ActionComponent) return ActionComponent + + if (content) { + return ( + + + + ) + } + + if (icon) { + return ( + + ) + } + + return +} + +const $wrapper: ViewStyle = { + height: 56, + alignItems: "center", + justifyContent: "space-between", +} + +const $container: ViewStyle = { + width: "100%", +} + +const $title: TextStyle = { + textAlign: "center", +} + +const $actionTextContainer: ThemedStyle = ({ spacing }) => ({ + flexGrow: 0, + alignItems: "center", + justifyContent: "center", + height: "100%", + paddingHorizontal: spacing.md, + zIndex: 2, +}) + +const $actionText: ThemedStyle = ({ colors }) => ({ + color: colors.tint, +}) + +const $actionIconContainer: ThemedStyle = ({ spacing }) => ({ + flexGrow: 0, + alignItems: "center", + justifyContent: "center", + height: "100%", + paddingHorizontal: spacing.md, + zIndex: 2, +}) + +const $actionFillerContainer: ViewStyle = { + width: 16, +} + +const $titleWrapperCenter: ThemedStyle = ({ spacing }) => ({ + alignItems: "center", + justifyContent: "center", + height: "100%", + width: "100%", + position: "absolute", + paddingHorizontal: spacing.xxl, + zIndex: 1, +}) + +const $titleWrapperFlex: ViewStyle = { + justifyContent: "center", + flexGrow: 1, +} diff --git a/app/components/Icon.tsx b/app/components/Icon.tsx new file mode 100644 index 00000000..bb80ca34 --- /dev/null +++ b/app/components/Icon.tsx @@ -0,0 +1,117 @@ +import { ComponentType } from "react" +import { + Image, + ImageStyle, + StyleProp, + TouchableOpacity, + TouchableOpacityProps, + View, + ViewProps, + ViewStyle, +} from "react-native" +import { useAppTheme } from "@/utils/useAppTheme" + +export type IconTypes = keyof typeof iconRegistry + +interface IconProps extends TouchableOpacityProps { + /** + * The name of the icon + */ + icon: IconTypes + + /** + * An optional tint color for the icon + */ + color?: string + + /** + * An optional size for the icon. If not provided, the icon will be sized to the icon's resolution. + */ + size?: number + + /** + * Style overrides for the icon image + */ + style?: StyleProp + + /** + * Style overrides for the icon container + */ + containerStyle?: StyleProp + + /** + * An optional function to be called when the icon is pressed + */ + onPress?: TouchableOpacityProps["onPress"] +} + +/** + * A component to render a registered icon. + * It is wrapped in a if `onPress` is provided, otherwise a . + * @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/components/Icon/} + * @param {IconProps} props - The props for the `Icon` component. + * @returns {JSX.Element} The rendered `Icon` component. + */ +export function Icon(props: IconProps) { + const { + icon, + color, + size, + style: $imageStyleOverride, + containerStyle: $containerStyleOverride, + ...WrapperProps + } = props + + const isPressable = !!WrapperProps.onPress + const Wrapper = (WrapperProps?.onPress ? TouchableOpacity : View) as ComponentType< + TouchableOpacityProps | ViewProps + > + + const { theme } = useAppTheme() + + const $imageStyle: StyleProp = [ + $imageStyleBase, + { tintColor: color ?? theme.colors.text }, + size !== undefined && { width: size, height: size }, + $imageStyleOverride, + ] + + return ( + + + + ) +} + +export const iconRegistry = { + back: require("../../assets/icons/back.png"), + bell: require("../../assets/icons/bell.png"), + caretLeft: require("../../assets/icons/caretLeft.png"), + caretRight: require("../../assets/icons/caretRight.png"), + check: require("../../assets/icons/check.png"), + clap: require("../../assets/icons/demo/clap.png"), + community: require("../../assets/icons/demo/community.png"), + components: require("../../assets/icons/demo/components.png"), + debug: require("../../assets/icons/demo/debug.png"), + github: require("../../assets/icons/demo/github.png"), + heart: require("../../assets/icons/demo/heart.png"), + hidden: require("../../assets/icons/hidden.png"), + ladybug: require("../../assets/icons/ladybug.png"), + lock: require("../../assets/icons/lock.png"), + menu: require("../../assets/icons/menu.png"), + more: require("../../assets/icons/more.png"), + pin: require("../../assets/icons/demo/pin.png"), + podcast: require("../../assets/icons/demo/podcast.png"), + settings: require("../../assets/icons/settings.png"), + slack: require("../../assets/icons/demo/slack.png"), + view: require("../../assets/icons/view.png"), + x: require("../../assets/icons/x.png"), +} + +const $imageStyleBase: ImageStyle = { + resizeMode: "contain", +} diff --git a/app/components/KeyboardDismisser.tsx b/app/components/KeyboardDismisser.tsx new file mode 100644 index 00000000..20d33a2f --- /dev/null +++ b/app/components/KeyboardDismisser.tsx @@ -0,0 +1,19 @@ +import { Keyboard, Pressable } from "react-native" +import { useKeyboard } from "@/hooks/useKeyboard" + +export const KeyboardDismisser = () => { + const { isOpened } = useKeyboard() + return ( + + ) +} diff --git a/app/components/Screen.tsx b/app/components/Screen.tsx new file mode 100644 index 00000000..4085fbfd --- /dev/null +++ b/app/components/Screen.tsx @@ -0,0 +1,304 @@ +import { StatusBar, StatusBarProps, StatusBarStyle } from "expo-status-bar" +import { ReactNode, useRef, useState } from "react" +import { + KeyboardAvoidingView, + KeyboardAvoidingViewProps, + LayoutChangeEvent, + Platform, + ScrollView, + ScrollViewProps, + StyleProp, + View, + ViewStyle, +} from "react-native" +import { KeyboardAwareScrollView } from "react-native-keyboard-controller" +import { useAppTheme } from "@/utils/useAppTheme" +import { useScrollToTop } from "@react-navigation/native" +import { $styles } from "../theme" +import { ExtendedEdge, useSafeAreaInsetsStyle } from "../utils/useSafeAreaInsetsStyle" + +export const DEFAULT_BOTTOM_OFFSET = 50 + +interface BaseScreenProps { + /** + * Children components. + */ + children?: ReactNode + /** + * Style for the outer content container useful for padding & margin. + */ + style?: StyleProp + /** + * Style for the inner content container useful for padding & margin. + */ + contentContainerStyle?: StyleProp + /** + * Override the default edges for the safe area. + */ + safeAreaEdges?: ExtendedEdge[] + /** + * Background color + */ + backgroundColor?: string + /** + * Status bar setting. Defaults to dark. + */ + statusBarStyle?: StatusBarStyle + /** + * By how much should we offset the keyboard? Defaults to 0. + */ + keyboardOffset?: number + /** + * By how much we scroll up when the keyboard is shown. Defaults to 50. + */ + keyboardBottomOffset?: number + /** + * Pass any additional props directly to the StatusBar component. + */ + StatusBarProps?: StatusBarProps + /** + * Pass any additional props directly to the KeyboardAvoidingView component. + */ + KeyboardAvoidingViewProps?: KeyboardAvoidingViewProps +} + +interface FixedScreenProps extends BaseScreenProps { + preset?: "fixed" +} +interface ScrollScreenProps extends BaseScreenProps { + preset?: "scroll" + /** + * Should keyboard persist on screen tap. Defaults to handled. + * Only applies to scroll preset. + */ + keyboardShouldPersistTaps?: "handled" | "always" | "never" + /** + * Pass any additional props directly to the ScrollView component. + */ + ScrollViewProps?: ScrollViewProps +} + +interface AutoScreenProps extends Omit { + preset?: "auto" + /** + * Threshold to trigger the automatic disabling/enabling of scroll ability. + * Defaults to `{ percent: 0.92 }`. + */ + scrollEnabledToggleThreshold?: { percent?: number; point?: number } +} + +export type ScreenProps = ScrollScreenProps | FixedScreenProps | AutoScreenProps + +const isIos = Platform.OS === "ios" + +type ScreenPreset = "fixed" | "scroll" | "auto" + +/** + * @param {ScreenPreset?} preset - The preset to check. + * @returns {boolean} - Whether the preset is non-scrolling. + */ +function isNonScrolling(preset?: ScreenPreset) { + return !preset || preset === "fixed" +} + +/** + * Custom hook that handles the automatic enabling/disabling of scroll ability based on the content size and screen size. + * @param {UseAutoPresetProps} props - The props for the `useAutoPreset` hook. + * @returns {{boolean, Function, Function}} - The scroll state, and the `onContentSizeChange` and `onLayout` functions. + */ +function useAutoPreset(props: AutoScreenProps): { + scrollEnabled: boolean + onContentSizeChange: (w: number, h: number) => void + onLayout: (e: LayoutChangeEvent) => void +} { + const { preset, scrollEnabledToggleThreshold } = props + const { percent = 0.92, point = 0 } = scrollEnabledToggleThreshold || {} + + const scrollViewHeight = useRef(null) + const scrollViewContentHeight = useRef(null) + const [scrollEnabled, setScrollEnabled] = useState(true) + + function updateScrollState() { + if (scrollViewHeight.current === null || scrollViewContentHeight.current === null) return + + // check whether content fits the screen then toggle scroll state according to it + const contentFitsScreen = (function () { + if (point) { + return scrollViewContentHeight.current < scrollViewHeight.current - point + } else { + return scrollViewContentHeight.current < scrollViewHeight.current * percent + } + })() + + // content is less than the size of the screen, so we can disable scrolling + if (scrollEnabled && contentFitsScreen) setScrollEnabled(false) + + // content is greater than the size of the screen, so let's enable scrolling + if (!scrollEnabled && !contentFitsScreen) setScrollEnabled(true) + } + + /** + * @param {number} w - The width of the content. + * @param {number} h - The height of the content. + */ + function onContentSizeChange(w: number, h: number) { + // update scroll-view content height + scrollViewContentHeight.current = h + updateScrollState() + } + + /** + * @param {LayoutChangeEvent} e = The layout change event. + */ + function onLayout(e: LayoutChangeEvent) { + const { height } = e.nativeEvent.layout + // update scroll-view height + scrollViewHeight.current = height + updateScrollState() + } + + // update scroll state on every render + if (preset === "auto") updateScrollState() + + return { + scrollEnabled: preset === "auto" ? scrollEnabled : true, + onContentSizeChange, + onLayout, + } +} + +/** + * @param {ScreenProps} props - The props for the `ScreenWithoutScrolling` component. + * @returns {JSX.Element} - The rendered `ScreenWithoutScrolling` component. + */ +function ScreenWithoutScrolling(props: ScreenProps) { + const { style, contentContainerStyle, children, preset } = props + return ( + + + {children} + + + ) +} + +/** + * @param {ScreenProps} props - The props for the `ScreenWithScrolling` component. + * @returns {JSX.Element} - The rendered `ScreenWithScrolling` component. + */ +function ScreenWithScrolling(props: ScreenProps) { + const { + children, + keyboardShouldPersistTaps = "handled", + keyboardBottomOffset = DEFAULT_BOTTOM_OFFSET, + contentContainerStyle, + ScrollViewProps, + style, + } = props as ScrollScreenProps + + const ref = useRef(null) + + const { scrollEnabled, onContentSizeChange, onLayout } = useAutoPreset(props as AutoScreenProps) + + // Add native behavior of pressing the active tab to scroll to the top of the content + // More info at: https://reactnavigation.org/docs/use-scroll-to-top/ + useScrollToTop(ref) + + return ( + { + onLayout(e) + ScrollViewProps?.onLayout?.(e) + }} + onContentSizeChange={(w: number, h: number) => { + onContentSizeChange(w, h) + ScrollViewProps?.onContentSizeChange?.(w, h) + }} + style={[$outerStyle, ScrollViewProps?.style, style]} + contentContainerStyle={[ + $innerStyle, + ScrollViewProps?.contentContainerStyle, + contentContainerStyle, + ]} + > + {children} + + ) +} + +/** + * Represents a screen component that provides a consistent layout and behavior for different screen presets. + * The `Screen` component can be used with different presets such as "fixed", "scroll", or "auto". + * It handles safe area insets, status bar settings, keyboard avoiding behavior, and scrollability based on the preset. + * @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/app/components/Screen/} + * @param {ScreenProps} props - The props for the `Screen` component. + * @returns {JSX.Element} The rendered `Screen` component. + */ +export function Screen(props: ScreenProps) { + const { + theme: { colors }, + themeContext, + } = useAppTheme() + const { + backgroundColor, + KeyboardAvoidingViewProps, + keyboardOffset = 0, + safeAreaEdges, + StatusBarProps, + statusBarStyle, + } = props + + const $containerInsets = useSafeAreaInsetsStyle(safeAreaEdges) + + return ( + + + + + {isNonScrolling(props.preset) ? ( + + ) : ( + + )} + + + ) +} + +const $containerStyle: ViewStyle = { + flex: 1, + height: "100%", + width: "100%", +} + +const $outerStyle: ViewStyle = { + flex: 1, + height: "100%", + width: "100%", +} + +const $justifyFlexEnd: ViewStyle = { + justifyContent: "flex-end", +} + +const $innerStyle: ViewStyle = { + justifyContent: "flex-start", + alignItems: "stretch", +} diff --git a/app/components/Text.tsx b/app/components/Text.tsx new file mode 100644 index 00000000..6b4f842c --- /dev/null +++ b/app/components/Text.tsx @@ -0,0 +1,113 @@ +import { TOptions } from "i18next" +import { StyleProp, Text as RNText, TextProps as RNTextProps, TextStyle } from "react-native" +import { isRTL, translate, TxKeyPath } from "../i18n" +import type { ThemedStyle, ThemedStyleArray } from "@/theme" +import { useAppTheme } from "@/utils/useAppTheme" +import { typography } from "@/theme/typography" +import { ReactNode } from "react" + +type Sizes = keyof typeof $sizeStyles +type Weights = keyof typeof typography.primary +type Presets = "default" | "bold" | "heading" | "subheading" | "formLabel" | "formHelper" + +export interface TextProps extends RNTextProps { + /** + * Text which is looked up via i18n. + */ + tx?: TxKeyPath + /** + * The text to display if not using `tx` or nested components. + */ + text?: string + /** + * Optional options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + txOptions?: TOptions + /** + * An optional style override useful for padding & margin. + */ + style?: StyleProp + /** + * One of the different types of text presets. + */ + preset?: Presets + /** + * Text weight modifier. + */ + weight?: Weights + /** + * Text size modifier. + */ + size?: Sizes + /** + * Children components. + */ + children?: ReactNode +} + +/** + * For your text displaying needs. + * This component is a HOC over the built-in React Native one. + * @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/components/Text/} + * @param {TextProps} props - The props for the `Text` component. + * @returns {JSX.Element} The rendered `Text` component. + */ +export function Text(props: TextProps) { + const { weight, size, tx, txOptions, text, children, style: $styleOverride, ...rest } = props + const { themed } = useAppTheme() + + const i18nText = tx && translate(tx, txOptions) + const content = i18nText || text || children + + const preset: Presets = props.preset ?? "default" + const $styles: StyleProp = [ + $rtlStyle, + themed($presets[preset]), + weight && $fontWeightStyles[weight], + size && $sizeStyles[size], + $styleOverride, + ] + + return ( + + {content} + + ) +} + +const $sizeStyles = { + xxl: { fontSize: 36, lineHeight: 44 } satisfies TextStyle, + xl: { fontSize: 24, lineHeight: 34 } satisfies TextStyle, + lg: { fontSize: 20, lineHeight: 32 } satisfies TextStyle, + md: { fontSize: 18, lineHeight: 26 } satisfies TextStyle, + sm: { fontSize: 16, lineHeight: 24 } satisfies TextStyle, + xs: { fontSize: 14, lineHeight: 21 } satisfies TextStyle, + xxs: { fontSize: 12, lineHeight: 18 } satisfies TextStyle, +} + +const $fontWeightStyles = Object.entries(typography.primary).reduce((acc, [weight, fontFamily]) => { + return { ...acc, [weight]: { fontFamily } } +}, {}) as Record + +const $baseStyle: ThemedStyle = (theme) => ({ + ...$sizeStyles.sm, + ...$fontWeightStyles.normal, + color: theme.colors.text, +}) + +const $presets: Record> = { + default: [$baseStyle], + bold: [$baseStyle, { ...$fontWeightStyles.bold }], + heading: [ + $baseStyle, + { + ...$sizeStyles.xxl, + ...$fontWeightStyles.bold, + }, + ], + subheading: [$baseStyle, { ...$sizeStyles.lg, ...$fontWeightStyles.medium }], + formLabel: [$baseStyle, { ...$fontWeightStyles.medium }], + formHelper: [$baseStyle, { ...$sizeStyles.sm, ...$fontWeightStyles.normal }], +} +const $rtlStyle: TextStyle = isRTL ? { writingDirection: "rtl" } : {} diff --git a/app/components/ThinkingAnimation.tsx b/app/components/ThinkingAnimation.tsx new file mode 100644 index 00000000..99d40da4 --- /dev/null +++ b/app/components/ThinkingAnimation.tsx @@ -0,0 +1,23 @@ +import React from "react" +import { Image, ImageStyle } from "react-native" +import { images } from "@/theme/images" + +interface ThinkingAnimationProps { + size?: number + style?: ImageStyle +} + +export const ThinkingAnimation = ({ size = 30, style }: ThinkingAnimationProps) => { + return ( + + ) +} \ No newline at end of file diff --git a/app/components/index.ts b/app/components/index.ts new file mode 100644 index 00000000..0424a605 --- /dev/null +++ b/app/components/index.ts @@ -0,0 +1,2 @@ +export * from './Header' +export * from './KeyboardDismisser' diff --git a/app/hooks/useAutoUpdate.ts b/app/hooks/useAutoUpdate.ts index 031d789e..890d02a3 100644 --- a/app/hooks/useAutoUpdate.ts +++ b/app/hooks/useAutoUpdate.ts @@ -4,7 +4,6 @@ import { useEffect } from "react" export const useAutoUpdate = () => { const handleCheckUpdate = async () => { if (__DEV__) { - console.log('Update checking disabled in development'); return; } @@ -28,7 +27,6 @@ export const useAutoUpdate = () => { useEffect(() => { if (__DEV__) { - console.log('Update checking disabled in development'); return; } diff --git a/app/hooks/useChat.ts b/app/hooks/useChat.ts new file mode 100644 index 00000000..adfc7134 --- /dev/null +++ b/app/hooks/useChat.ts @@ -0,0 +1,142 @@ +import { fetch as expoFetch } from "expo/fetch" +import { useEffect, useRef, useState } from "react" +import { useStores } from "@/models" +import { Message, useChat as useVercelChat } from "@ai-sdk/react" +import Config from "../config" + +export function useChat() { + const { chatStore, coderStore } = useStores() + const pendingToolInvocations = useRef([]) + const [localMessages, setLocalMessages] = useState([]) + + const { isLoading, messages: aiMessages, error, append, setMessages } = useVercelChat({ + fetch: expoFetch as unknown as typeof globalThis.fetch, + api: Config.NEXUS_URL, + body: { + ...(coderStore.githubToken && + chatStore.enabledTools.length > 0 && { + githubToken: coderStore.githubToken, + tools: chatStore.enabledTools, + repos: coderStore.repos.map((repo) => ({ + owner: repo.owner, + name: repo.name, + branch: repo.branch, + })), + }), + }, + onError: (error) => { + console.error(error, "ERROR") + chatStore.setError(error.message || "An error occurred") + }, + onToolCall: async (toolCall) => { + // console.log("TOOL CALL", toolCall) + }, + onFinish: (message, options) => { + chatStore.setIsGenerating(false) + if (message.role === "assistant") { + chatStore.addMessage({ + role: "assistant", + content: message.content, + metadata: { + conversationId: chatStore.currentConversationId, + usage: options.usage, + finishReason: options.finishReason, + toolInvocations: pendingToolInvocations.current, + }, + }) + pendingToolInvocations.current = [] + } + }, + }) + + // Watch messages for tool invocations + useEffect(() => { + const lastMessage = aiMessages[aiMessages.length - 1] + if (!lastMessage || !lastMessage.toolInvocations) return + if (lastMessage?.role === "assistant" && lastMessage.toolInvocations?.length > 0) { + // console.log("Found tool invocations:", lastMessage.toolInvocations) + pendingToolInvocations.current = lastMessage.toolInvocations + } + }, [aiMessages]) + + // Sync store messages with local state - with memoization to prevent unnecessary updates + useEffect(() => { + const storedMessages = chatStore.currentMessages + const chatMessages = storedMessages.map((msg) => ({ + id: msg.id, + role: msg.role as "user" | "assistant" | "system", + content: msg.content, + createdAt: new Date(msg.createdAt), + ...(msg.metadata?.toolInvocations + ? { + toolInvocations: msg.metadata.toolInvocations, + } + : {}), + })) + + // Only update if messages have actually changed + if (JSON.stringify(chatMessages) !== JSON.stringify(localMessages)) { + setLocalMessages(chatMessages) + } + }, [chatStore.currentMessages]) + + // Load persisted messages when conversation changes - with cleanup + useEffect(() => { + let mounted = true + + const loadMessages = async () => { + if (!mounted) return + + setMessages([]) + const storedMessages = chatStore.currentMessages + if (storedMessages.length > 0) { + const chatMessages = storedMessages.map((msg) => ({ + id: msg.id, + role: msg.role as "user" | "assistant" | "system", + content: msg.content, + createdAt: new Date(msg.createdAt), + ...(msg.metadata?.toolInvocations + ? { + toolInvocations: msg.metadata.toolInvocations, + } + : {}), + })) + if (mounted) { + setMessages(chatMessages) + } + } + } + + loadMessages() + + return () => { + mounted = false + } + }, [chatStore.currentConversationId]) + + const handleSendMessage = async (message: string) => { + pendingToolInvocations.current = [] + + chatStore.addMessage({ + role: "user", + content: message, + metadata: { + conversationId: chatStore.currentConversationId, + }, + }) + + chatStore.setIsGenerating(true) + + await append({ + content: message, + role: "user", + createdAt: new Date(), + }) + } + + return { + handleSendMessage, + isLoading, + messages: localMessages, + } +} diff --git a/app/hooks/useHeader.tsx b/app/hooks/useHeader.tsx new file mode 100644 index 00000000..a363cfe1 --- /dev/null +++ b/app/hooks/useHeader.tsx @@ -0,0 +1,27 @@ +import { useLayoutEffect } from "react" +import { useNavigation } from "@react-navigation/native" +import { Header, HeaderProps } from "../components" + +/** + * A hook that can be used to easily set the Header of a react-navigation screen from within the screen's component. + * @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/utility/useHeader/} + * @param {HeaderProps} headerProps - The props for the `Header` component. + * @param {any[]} deps - The dependencies to watch for changes to update the header. + */ +export function useHeader( + headerProps: HeaderProps, + deps: Parameters[1] = [], +) { + const navigation = useNavigation() + + // To avoid a visible header jump when navigating between screens, we use + // `useLayoutEffect`, which will apply the settings before the screen renders. + useLayoutEffect(() => { + navigation.setOptions({ + headerShown: true, + header: () =>
, + }) + // intentionally created API to have user set when they want to update the header via `deps` + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [...deps, navigation]) +} diff --git a/app/hooks/useKeyboard.ts b/app/hooks/useKeyboard.ts new file mode 100644 index 00000000..95d0ea5d --- /dev/null +++ b/app/hooks/useKeyboard.ts @@ -0,0 +1,116 @@ +import { useEffect, useRef, useState } from "react" +import { Keyboard, Platform, TextInput } from "react-native" + +// Module-level singleton state +let isKeyboardOpening = false +let isKeyboardOpened = false +let listeners: Set<(isOpened: boolean) => void> = new Set() +let isInitialized = false +let globalInputRef: TextInput | null = null + +export function useKeyboard() { + const [isOpened, setIsOpened] = useState(isKeyboardOpened) + const localRef = useRef(null) + + useEffect(() => { + // Add component's state setter to listeners + listeners.add(setIsOpened) + + if (!isInitialized) { + // Set up listeners only once + const showEvent = Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow" + const hideEvent = Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide" + + const showListener = Keyboard.addListener(showEvent, () => { + isKeyboardOpening = true + isKeyboardOpened = true + listeners.forEach(listener => listener(true)) + }) + + const hideListener = Keyboard.addListener(hideEvent, () => { + isKeyboardOpening = false + isKeyboardOpened = false + listeners.forEach(listener => listener(false)) + }) + + isInitialized = true + + // Cleanup on app unmount + return () => { + showListener.remove() + hideListener.remove() + isInitialized = false + listeners.clear() + globalInputRef = null + } + } + + // Cleanup component's listener and ref + return () => { + listeners.delete(setIsOpened) + if (globalInputRef === localRef.current) { + globalInputRef = null + } + } + }, []) + + // Update global ref whenever local ref changes + useEffect(() => { + const checkRef = () => { + if (localRef.current) { + globalInputRef = localRef.current + } + } + + // Check immediately + checkRef() + + // And after a short delay to ensure mounting + const timer = setTimeout(checkRef, 100) + + return () => clearTimeout(timer) + }, []) + + const show = () => { + if (globalInputRef) { + // Force keyboard to show + if (Platform.OS === 'ios') { + globalInputRef.focus() + } else { + // On Android, sometimes need to blur then focus + globalInputRef.blur() + setTimeout(() => { + if (globalInputRef) { + globalInputRef.focus() + } + }, 50) + } + } else { + // If ref not available, try again after a short delay + setTimeout(() => { + console.log('retrying focus', !!globalInputRef) + if (globalInputRef) { + console.log('focusing (retry)') + if (Platform.OS === 'ios') { + globalInputRef.focus() + } else { + globalInputRef.blur() + setTimeout(() => { + if (globalInputRef) { + globalInputRef.focus() + } + }, 50) + } + } + }, 50) + } + } + + return { + isOpening: isKeyboardOpening, + isOpened, + dismiss: Keyboard.dismiss, + show, + ref: localRef, + } +} diff --git a/app/hooks/useVoiceRecording.ts b/app/hooks/useVoiceRecording.ts new file mode 100644 index 00000000..17477f68 --- /dev/null +++ b/app/hooks/useVoiceRecording.ts @@ -0,0 +1,125 @@ +import { Audio } from "expo-av" +import { useEffect, useRef, useState } from "react" +import { groqChatApi } from "@/services/groq/groq-chat" +import { log } from "@/utils/log" +import { useVoicePermissions } from "./useVoicePermissions" + +export const useVoiceRecording = (onTranscription: (text: string) => void) => { + const [isRecording, setIsRecording] = useState(false) + const [error, setError] = useState("") + const [isProcessing, setIsProcessing] = useState(false) + const recording = useRef(null) + const { hasPermission, requestPermissions } = useVoicePermissions() + + useEffect(() => { + return () => { + stopRecording().catch(err => { + log.error( + "[useVoiceRecording] Cleanup error: " + + (err instanceof Error ? err.message : String(err)) + ) + }) + } + }, []) + + const setupAudioMode = async () => { + try { + await Audio.setAudioModeAsync({ + allowsRecordingIOS: true, + playsInSilentModeIOS: true, + }) + } catch (err) { + throw new Error("Failed to set audio mode: " + String(err)) + } + } + + const startRecording = async () => { + try { + setError("") + + if (!hasPermission) { + const granted = await requestPermissions() + if (!granted) { + setError("Microphone permission is required") + return false + } + } + + await setupAudioMode() + + const { recording: newRecording } = await Audio.Recording.createAsync( + Audio.RecordingOptionsPresets.HIGH_QUALITY + ) + recording.current = newRecording + setIsRecording(true) + return true + } catch (err) { + setError("Failed to start recording") + log.error( + "[useVoiceRecording] Start error: " + + (err instanceof Error ? err.message : String(err)) + ) + return false + } + } + + const stopRecording = async () => { + if (!recording.current) return + + try { + const currentRecording = recording.current + recording.current = null + setIsRecording(false) + setIsProcessing(true) + + await currentRecording.stopAndUnloadAsync() + const uri = currentRecording.getURI() + + if (uri) { + const result = await groqChatApi.transcribeAudio(uri, { + model: "whisper-large-v3", + language: "en", + }) + + if (result.kind === "ok") { + onTranscription(result.response.text) + } else { + setError("Transcription failed") + log.error("[useVoiceRecording] Transcription error: " + JSON.stringify(result)) + } + } + } catch (err) { + setError("Failed to process recording") + log.error( + "[useVoiceRecording] Stop error: " + + (err instanceof Error ? err.message : String(err)) + ) + } finally { + setIsProcessing(false) + } + } + + const cancelRecording = async () => { + if (!recording.current) return + + try { + await recording.current.stopAndUnloadAsync() + recording.current = null + setIsRecording(false) + } catch (err) { + log.error( + "[useVoiceRecording] Cancel error: " + + (err instanceof Error ? err.message : String(err)) + ) + } + } + + return { + isRecording, + isProcessing, + error, + startRecording, + stopRecording, + cancelRecording + } +} diff --git a/app/i18n/ar.ts b/app/i18n/ar.ts new file mode 100644 index 00000000..89f1124a --- /dev/null +++ b/app/i18n/ar.ts @@ -0,0 +1,125 @@ +import demoAr from "./demo-ar" +import { Translations } from "./en" + +const ar: Translations = { + common: { + ok: "نعم", + cancel: "حذف", + back: "خلف", + logOut: "تسجيل خروج", + }, + welcomeScreen: { + postscript: + "ربما لا يكون هذا هو الشكل الذي يبدو عليه تطبيقك مالم يمنحك المصمم هذه الشاشات وشحنها في هذه الحالة", + readyForLaunch: "تطبيقك تقريبا جاهز للتشغيل", + exciting: "اوه هذا مثير", + letsGo: "لنذهب", + }, + errorScreen: { + title: "هناك خطأ ما", + friendlySubtitle: + "هذه هي الشاشة التي سيشاهدها المستخدمون في عملية الانتاج عند حدوث خطأ. سترغب في تخصيص هذه الرسالة ( الموجودة في 'ts.en/i18n/app') وربما التخطيط ايضاً ('app/screens/ErrorScreen'). إذا كنت تريد إزالة هذا بالكامل، تحقق من 'app/app.tsp' من اجل عنصر .", + reset: "اعادة تعيين التطبيق", + traceTitle: "خطأ من مجموعة %{name}", + }, + emptyStateComponent: { + generic: { + heading: "فارغة جداً....حزين", + content: "لا توجد بيانات حتى الآن. حاول النقر فوق الزر لتحديث التطبيق او اعادة تحميله.", + button: "لنحاول هذا مرّة أخرى", + }, + }, + + errors: { + invalidEmail: "عنوان البريد الالكتروني غير صالح", + }, + loginScreen: { + logIn: "تسجيل الدخول", + enterDetails: + ".ادخل التفاصيل الخاصة بك ادناه لفتح معلومات سرية للغاية. لن تخمن ابداً ما الذي ننتظره. او ربما ستفعل انها انها ليست علم الصواريخ", + emailFieldLabel: "البريد الالكتروني", + passwordFieldLabel: "كلمة السر", + emailFieldPlaceholder: "ادخل بريدك الالكتروني", + passwordFieldPlaceholder: "كلمة السر هنا فائقة السر", + tapToLogIn: "انقر لتسجيل الدخول!", + hint: "(: تلميح: يمكنك استخدام اي عنوان بريد الكتروني وكلمة السر المفضلة لديك", + }, + demoNavigator: { + componentsTab: "عناصر", + debugTab: "تصحيح", + communityTab: "واصل اجتماعي", + podcastListTab: "البودكاست", + }, + demoCommunityScreen: { + title: "تواصل مع المجتمع", + tagLine: + "قم بالتوصيل لمنتدى Infinite Red الذي يضم تفاعل المهندسين المحلّيين ورفع مستوى تطوير تطبيقك معنا", + joinUsOnSlackTitle: "انضم الينا على Slack", + joinUsOnSlack: + "هل ترغب في وجود مكان للتواصل مع مهندسي React Native حول العالم؟ الانضمام الى المحادثة في سلاك المجتمع الاحمر اللانهائي! مجتمعناالمتنامي هو مساحةآمنة لطرح الاسئلة والتعلم من الآخرين وتنمية شبكتك.", + joinSlackLink: "انضم الي مجتمع Slack", + makeIgniteEvenBetterTitle: "اجعل Ignite افضل", + makeIgniteEvenBetter: + "هل لديك فكرة لجعل Ignite افضل؟ نحن سعداء لسماع ذلك! نحن نبحث دائماً عن الآخرين الذين يرغبون في مساعدتنا في بناء افضل الادوات المحلية التفاعلية المتوفرة هناك. انضم الينا عبر GitHub للانضمام الينا في بناء مستقبل Ignite", + contributeToIgniteLink: "ساهم في Ignite", + theLatestInReactNativeTitle: "الاحدث في React Native", + theLatestInReactNative: "نخن هنا لنبقيك محدثاً على جميع React Native التي تعرضها", + reactNativeRadioLink: "راديو React Native", + reactNativeNewsletterLink: "نشرة اخبار React Native", + reactNativeLiveLink: "مباشر React Native", + chainReactConferenceLink: "مؤتمر Chain React", + hireUsTitle: "قم بتوظيف Infinite Red لمشروعك القادم", + hireUs: + "سواء كان الامر يتعلّق بتشغيل مشروع كامل او اعداد الفرق بسرعة من خلال التدريب العلمي لدينا، يمكن ان يساعد Infinite Red اللامتناهي في اي مشروع محلي يتفاعل معه.", + hireUsLink: "ارسل لنا رسالة", + }, + demoShowroomScreen: { + jumpStart: "مكونات او عناصر لبدء مشروعك", + lorem2Sentences: + "عامل الناس بأخلاقك لا بأخلاقهم. عامل الناس بأخلاقك لا بأخلاقهم. عامل الناس بأخلاقك لا بأخلاقهم", + demoHeaderTxExample: "ياي", + demoViaTxProp: "عبر `tx` Prop", + demoViaSpecifiedTxProp: "Prop `{{prop}}Tx` عبر", + }, + demoDebugScreen: { + howTo: "كيف", + title: "التصحيح", + tagLine: "مبروك، لديك نموذج اصلي متقدم للغاية للتفاعل هنا. الاستفادة من هذه النمذجة", + reactotron: "Reactotron ارسل إلى", + reportBugs: "الابلاغ عن اخطاء", + demoList: "قائمة تجريبية", + demoPodcastList: "قائمة البودكاست التجريبي", + androidReactotronHint: + "اذا لم ينجح ذللك، فتأكد من تشغيل تطبيق الحاسوب الخاص Reactotron، وقم بتشغيل عكس adb tcp:9090 \ntcp:9090 من جهازك الطرفي ، واعد تحميل التطبيق", + iosReactotronHint: + "اذا لم ينجح ذلك، فتأكد من تشغيل تطبيق الحاسوب الخاص ب Reactotron وأعد تحميل التطبيق", + macosReactotronHint: "اذا لم ينجح ذلك، فتأكد من تشغيل الحاسوب ب Reactotron وأعد تحميل التطبيق", + webReactotronHint: "اذا لم ينجح ذلك، فتأكد من تشغيل الحاسوب ب Reactotron وأعد تحميل التطبيق", + windowsReactotronHint: + "اذا لم ينجح ذلك، فتأكد من تشغيل الحاسوب ب Reactotron وأعد تحميل التطبيق", + }, + demoPodcastListScreen: { + title: "حلقات إذاعية React Native", + onlyFavorites: "المفضلة فقط", + favoriteButton: "المفضل", + unfavoriteButton: "غير مفضل", + accessibility: { + cardHint: "انقر مرّتين للاستماع على الحلقة. انقر مرّتين وانتظر لتفعيل {{action}} هذه الحلقة.", + switch: "قم بالتبديل لاظهار المفضّلة فقط.", + favoriteAction: "تبديل المفضلة", + favoriteIcon: "الحلقة الغير مفضّلة", + unfavoriteIcon: "الحلقة المفضّلة", + publishLabel: "نشرت {{date}}", + durationLabel: "المدّة: {{hours}} ساعات {{minutes}} دقائق {{seconds}} ثواني", + }, + noFavoritesEmptyState: { + heading: "هذا يبدو فارغاً بعض الشيء.", + content: + "لم تتم اضافة اي مفضلات حتى الان. اضغط على القلب في إحدى الحلقات لإضافته الى المفضلة.", + }, + }, + + ...demoAr, +} + +export default ar diff --git a/app/i18n/demo-ar.ts b/app/i18n/demo-ar.ts new file mode 100644 index 00000000..b3610096 --- /dev/null +++ b/app/i18n/demo-ar.ts @@ -0,0 +1,462 @@ +import { DemoTranslations } from "./demo-en" + +export const demoAr: DemoTranslations = { + demoIcon: { + description: + "مكون لعرض أيقونة مسجلة.يتم تغليفه في يتم توفير 'OnPress'، وإلا يتم توفير if `onPress` is provided, otherwise a .", + useCase: { + icons: { + name: "Icons", + description: "List of icons registered inside the component.", + }, + size: { + name: "Size", + description: "There's a size prop.", + }, + color: { + name: "Color", + description: "There's a color prop.", + }, + styling: { + name: "Styling", + description: "The component can be styled easily.", + }, + }, + }, + demoTextField: { + description: "TextField component allows for the entering and editing of text.", + useCase: { + statuses: { + name: "Statuses", + description: + "There is a status prop - similar to `preset` in other components, but affects component functionality as well.", + noStatus: { + label: "No Status", + helper: "This is the default status", + placeholder: "Text goes here", + }, + error: { + label: "Error Status", + helper: "Status to use when there is an error", + placeholder: "Text goes here", + }, + disabled: { + label: "Disabled Status", + helper: "Disables the editability and mutes text", + placeholder: "Text goes here", + }, + }, + passingContent: { + name: "Passing Content", + description: "There are a few different ways to pass content.", + viaLabel: { + labelTx: "Via `label` prop", + helper: "Via `helper` prop", + placeholder: "Via `placeholder` prop", + }, + rightAccessory: { + label: "RightAccessory", + helper: "This prop takes a function that returns a React element.", + }, + leftAccessory: { + label: "LeftAccessory", + helper: "This prop takes a function that returns a React element.", + }, + supportsMultiline: { + label: "Supports Multiline", + helper: "Enables a taller input for multiline text.", + }, + }, + styling: { + name: "Styling", + description: "The component can be styled easily.", + styleInput: { + label: "Style Input", + helper: "Via `style` prop", + }, + styleInputWrapper: { + label: "Style Input Wrapper", + helper: "Via `inputWrapperStyle` prop", + }, + styleContainer: { + label: "Style Container", + helper: "Via `containerStyle` prop", + }, + styleLabel: { + label: "Style Label & Helper", + helper: "Via `LabelTextProps` & `HelperTextProps` style prop", + }, + styleAccessories: { + label: "Style Accessories", + helper: "Via `RightAccessory` & `LeftAccessory` style prop", + }, + }, + }, + }, + demoToggle: { + description: + "Renders a boolean input. This is a controlled component that requires an onValueChange callback that updates the value prop in order for the component to reflect user actions. If the value prop is not updated, the component will continue to render the supplied value prop instead of the expected result of any user actions.", + useCase: { + variants: { + name: "Variants", + description: + "The component supports a few different variants. If heavy customization of a specific variant is needed, it can be easily refactored. The default is `checkbox`.", + checkbox: { + label: "`checkbox` variant", + helper: "This can be used for a single on/off input.", + }, + radio: { + label: "`radio` variant", + helper: "Use this when you have multiple options.", + }, + switch: { + label: "`switch` variant", + helper: "A more prominent on/off input. Has better accessibility support.", + }, + }, + statuses: { + name: "Statuses", + description: + "There is a status prop - similar to `preset` in other components, but affects component functionality as well.", + noStatus: "No status - this is the default", + errorStatus: "Error status - use when there is an error", + disabledStatus: "Disabled status - disables the editability and mutes input", + }, + passingContent: { + name: "Passing Content", + description: "There are a few different ways to pass content.", + useCase: { + checkBox: { + label: "Via `labelTx` prop", + helper: "Via `helperTx` prop.", + }, + checkBoxMultiLine: { + helper: "Supports multiline - Nulla proident consectetur labore sunt ea labore. ", + }, + radioChangeSides: { + helper: "You can change sides - Laborum labore adipisicing in eu ipsum deserunt.", + }, + customCheckBox: { + label: "Pass in a custom checkbox icon.", + }, + switch: { + label: "Switches can be read as text", + helper: + "By default, this option doesn't use `Text` since depending on the font, the on/off characters might look weird. Customize as needed.", + }, + switchAid: { + label: "Or aided with an icon", + }, + }, + }, + styling: { + name: "Styling", + description: "The component can be styled easily.", + outerWrapper: "1 - style the input outer wrapper", + innerWrapper: "2 - style the input inner wrapper", + inputDetail: "3 - style the input detail", + labelTx: "You can also style the labelTx", + styleContainer: "Or, style the entire container", + }, + }, + }, + demoButton: { + description: + "A component that allows users to take actions and make choices. Wraps the Text component with a Pressable component.", + useCase: { + presets: { + name: "Presets", + description: "There are a few presets that are preconfigured.", + }, + passingContent: { + name: "Passing Content", + description: "There are a few different ways to pass content.", + viaTextProps: "Via `text` Prop - Billum In", + children: "Children - Irure Reprehenderit", + rightAccessory: "RightAccessory - Duis Quis", + leftAccessory: "LeftAccessory - Duis Proident", + nestedChildren: "Nested children - proident veniam.", + nestedChildren2: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + nestedChildren3: "Occaecat aliqua irure proident veniam.", + multiLine: + "Multiline - consequat veniam veniam reprehenderit. Fugiat id nisi quis duis sunt proident mollit dolor mollit adipisicing proident deserunt.", + }, + styling: { + name: "Styling", + description: "The component can be styled easily.", + styleContainer: "Style Container - Exercitation", + styleText: "Style Text - Ea Anim", + styleAccessories: "Style Accessories - enim ea id fugiat anim ad.", + pressedState: "Style Pressed State - fugiat anim", + }, + disabling: { + name: "Disabling", + description: + "The component can be disabled, and styled based on that. Press behavior will be disabled.", + standard: "Disabled - standard", + filled: "Disabled - filled", + reversed: "Disabled - reversed", + accessory: "Disabled accessory style", + textStyle: "Disabled text style", + }, + }, + }, + demoListItem: { + description: "A styled row component that can be used in FlatList, SectionList, or by itself.", + useCase: { + height: { + name: "Height", + description: "The row can be different heights.", + defaultHeight: "Default height (56px)", + customHeight: "Custom height via `height` prop", + textHeight: + "Height determined by text content - Reprehenderit incididunt deserunt do do ea labore.", + longText: + "Limit long text to one line - Reprehenderit incididunt deserunt do do ea labore.", + }, + separators: { + name: "Separators", + description: "The separator / divider is preconfigured and optional.", + topSeparator: "Only top separator", + topAndBottomSeparator: "Top and bottom separators", + bottomSeparator: "Only bottom separator", + }, + icons: { + name: "Icons", + description: "You can customize the icons on the left or right.", + leftIcon: "Left icon", + rightIcon: "Right Icon", + leftRightIcons: "Left & Right Icons", + }, + customLeftRight: { + name: "Custom Left/Right Components", + description: "If you need a custom left/right component, you can pass it in.", + customLeft: "Custom left component", + customRight: "Custom right component", + }, + passingContent: { + name: "Passing Content", + description: "There are a few different ways to pass content.", + text: "Via `text` prop - reprehenderit sint", + children: "Children - mostrud mollit", + nestedChildren1: "Nested children - proident veniam.", + nestedChildren2: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + }, + listIntegration: { + name: "Integrating w/ FlatList & FlashList", + description: "The component can be easily integrated with your favorite list interface.", + }, + styling: { + name: "Styling", + description: "The component can be styled easily.", + styledText: "Styled Text", + styledContainer: "Styled Container (separators)", + tintedIcons: "Tinted Icons", + }, + }, + }, + demoCard: { + description: + "Cards are useful for displaying related information in a contained way. If a ListItem displays content horizontally, a Card can be used to display content vertically.", + useCase: { + presets: { + name: "Presets", + description: "There are a few presets that are preconfigured.", + default: { + heading: "Default Preset (default)", + content: "Incididunt magna ut aliquip consectetur mollit dolor.", + footer: "Consectetur nulla non aliquip velit.", + }, + reversed: { + heading: "Reversed Preset", + content: "Reprehenderit occaecat proident amet id laboris.", + footer: "Consectetur tempor ea non labore anim .", + }, + }, + verticalAlignment: { + name: "Vertical Alignment", + description: + "Depending on what's required, the card comes preconfigured with different alignment strategies.", + top: { + heading: "Top (default)", + content: "All content is automatically aligned to the top.", + footer: "Even the footer", + }, + center: { + heading: "Center", + content: "Content is centered relative to the card's height.", + footer: "Me too!", + }, + spaceBetween: { + heading: "Space Between", + content: "All content is spaced out evenly.", + footer: "I am where I want to be.", + }, + reversed: { + heading: "Force Footer Bottom", + content: "This pushes the footer where it belongs.", + footer: "I'm so lonely down here.", + }, + }, + passingContent: { + name: "Passing Content", + description: "There are a few different ways to pass content.", + heading: "Via `heading` Prop", + content: "Via `content` Prop", + footer: "I'm so lonely down here.", + }, + customComponent: { + name: "Custom Components", + description: + "Any of the preconfigured components can be replaced with your own. You can also add additional ones.", + rightComponent: "RightComponent", + leftComponent: "LeftComponent", + }, + style: { + name: "Styling", + description: "The component can be styled easily.", + heading: "Style the Heading", + content: "Style the Content", + footer: "Style the Footer", + }, + }, + }, + demoAutoImage: { + description: "An Image component that automatically sizes a remote or data-uri image.", + useCase: { + remoteUri: { name: "Remote URI" }, + base64Uri: { name: "Base64 URI" }, + scaledToFitDimensions: { + name: "Scaled to Fit Dimensions", + description: + "Providing a `maxWidth` and/or `maxHeight` props, the image will automatically scale while retaining it's aspect ratio. How is this different from `resizeMode: 'contain'`? Firstly, you can specify only one side's size (not both). Secondly, the image will scale to fit the desired dimensions instead of just being contained within its image-container.", + heightAuto: "width: 60 / height: auto", + widthAuto: "width: auto / height: 32", + bothManual: "width: 60 / height: 60", + }, + }, + }, + demoText: { + description: + "For your text displaying needs. This component is a HOC over the built-in React Native one.", + useCase: { + presets: { + name: "Presets", + description: "There are a few presets that are preconfigured.", + default: + "default preset - Cillum eu laboris in labore. Excepteur mollit tempor reprehenderit fugiat elit et eu consequat laborum.", + bold: "bold preset - Tempor et ullamco cupidatat in officia. Nulla ea duis elit id sunt ipsum cillum duis deserunt nostrud ut nostrud id.", + subheading: "subheading preset - In Cupidatat Cillum.", + heading: "heading preset - Voluptate Adipis.", + }, + sizes: { + name: "Sizes", + description: "There's a size prop.", + xs: "xs - Ea ipsum est ea ex sunt.", + sm: "sm - Lorem sunt adipisicin.", + md: "md - Consequat id do lorem.", + lg: "lg - Nostrud ipsum ea.", + xl: "xl - Eiusmod ex excepteur.", + xxl: "xxl - Cillum eu laboris.", + }, + weights: { + name: "Weights", + description: "There's a weight prop.", + light: + "light - Nulla magna incididunt excepteur est occaecat duis culpa dolore cupidatat enim et.", + normal: + "normal - Magna incididunt dolor ut veniam veniam laboris aliqua velit ea incididunt.", + medium: "medium - Non duis laborum quis laboris occaecat culpa cillum.", + semibold: "semiBold - Exercitation magna nostrud pariatur laborum occaecat aliqua.", + bold: "bold - Eiusmod ullamco magna exercitation est excepteur.", + }, + passingContent: { + name: "Passing Content", + description: "There are a few different ways to pass content.", + viaText: + "via `text` prop - Billum in aute fugiat proident nisi pariatur est. Cupidatat anim cillum eiusmod ad. Officia eu magna aliquip labore dolore consequat.", + viaTx: "via `tx` prop -", + children: "children - Aliqua velit irure reprehenderit eu qui amet veniam consectetur.", + nestedChildren: "Nested children -", + nestedChildren2: "Occaecat aliqua irure proident veniam.", + nestedChildren3: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + nestedChildren4: "Occaecat aliqua irure proident veniam.", + }, + styling: { + name: "Styling", + description: "The component can be styled easily.", + text: "Consequat ullamco veniam velit mollit proident excepteur aliquip id culpa ipsum velit sint nostrud.", + text2: + "Eiusmod occaecat laboris eu ex veniam ipsum adipisicing consectetur. Magna ullamco adipisicing tempor adipisicing.", + text3: + "Eiusmod occaecat laboris eu ex veniam ipsum adipisicing consectetur. Magna ullamco adipisicing tempor adipisicing.", + }, + }, + }, + demoHeader: { + description: + "Component that appears on many screens. Will hold navigation buttons and screen title.", + useCase: { + actionIcons: { + name: "Action Icons", + description: "You can easily pass in icons to the left or right action components.", + leftIconTitle: "Left Icon", + rightIconTitle: "Right Icon", + bothIconsTitle: "Both Icons", + }, + actionText: { + name: "Action Text", + description: "You can easily pass in text to the left or right action components.", + leftTxTitle: "Via `leftTx`", + rightTextTitle: "Via `rightText`", + }, + customActionComponents: { + name: "Custom Action Components", + description: + "If the icon or text options are not enough, you can pass in your own custom action component.", + customLeftActionTitle: "Custom Left Action", + }, + titleModes: { + name: "Title Modes", + description: + "Title can be forced to stay in center (default) but may be cut off if it's too long. You can optionally make it adjust to the action buttons.", + centeredTitle: "Centered Title", + flexTitle: "Flex Title", + }, + styling: { + name: "Styling", + description: "The component can be styled easily.", + styledTitle: "Styled Title", + styledWrapperTitle: "Styled Wrapper", + tintedIconsTitle: "Tinted Icons", + }, + }, + }, + demoEmptyState: { + description: + "A component to use when there is no data to display. It can be utilized to direct the user what to do next", + useCase: { + presets: { + name: "Presets", + description: + "You can create different text/image sets. One is predefined called `generic`. Note, there's no default in case you want to have a completely custom EmptyState.", + }, + passingContent: { + name: "Passing Content", + description: "There are a few different ways to pass content.", + customizeImageHeading: "Customize Image", + customizeImageContent: "You can pass in any image source.", + viaHeadingProp: "Via `heading` Prop", + viaContentProp: "Via `content` prop.", + viaButtonProp: "Via `button` Prop", + }, + styling: { + name: "Styling", + description: "The component can be styled easily.", + }, + }, + }, +} + +export default demoEn +export type DemoTranslations = typeof demoEn diff --git a/app/i18n/demo-es.ts b/app/i18n/demo-es.ts new file mode 100644 index 00000000..23d91772 --- /dev/null +++ b/app/i18n/demo-es.ts @@ -0,0 +1,467 @@ +import { DemoTranslations } from "./demo-en" + +export const demoEs: DemoTranslations = { + demoIcon: { + description: + "Un componente para dibujar un ícono pre-definido. Si se proporciona el atributo `onPress`, se rodea por un componente . De lo contrario, se rodea por un componente .", + useCase: { + icons: { + name: "Íconos", + description: "Lista de los íconos pre-definidos para el componente.", + }, + size: { + name: "Tamaño", + description: "Hay un atributo para el tamaño.", + }, + color: { + name: "Color", + description: "Hay un atributo para el color.", + }, + styling: { + name: "Estilo", + description: "El componente puede ser configurado fácilmente.", + }, + }, + }, + demoTextField: { + description: "El componente permite el ingreso y edición de texto.", + useCase: { + statuses: { + name: "Estados", + description: + "Hay un atributo para el estado - similar a `preset` en otros componentes, pero que además impacta en la funcionalidad del componente.", + noStatus: { + label: "Sin estado", + helper: "Este es el estado por defecto", + placeholder: "El texto va acá", + }, + error: { + label: "Estado de error", + helper: "Estado para usar en caso de error", + placeholder: "El texto va acá", + }, + disabled: { + label: "Estado desactivado", + helper: "Desactiva la edición y atenúa el texto", + placeholder: "El texto va acá", + }, + }, + passingContent: { + name: "Entregando contenido", + description: "Hay varias formas de entregar contenido.", + viaLabel: { + labelTx: "A través del atributo `label`", + helper: "A través del atributo `helper`", + placeholder: "A través del atributo `placeholder`", + }, + rightAccessory: { + label: "Complemento derecho", + helper: "Este atributo requiere una función que retorne un elemento React.", + }, + leftAccessory: { + label: "Complemento izquierdo", + helper: "Este atributo requiere una función que retorne un elemento React.", + }, + supportsMultiline: { + label: "Soporta múltilíneas", + helper: "Permite un input de texto más largo para texto multilinea.", + }, + }, + styling: { + name: "Estilo", + description: "El componente puede ser configurado fácilmente.", + styleInput: { + label: "Estilo del input", + helper: "A través de el atributo `style`", + }, + styleInputWrapper: { + label: "Estilo del contenedor del input", + helper: "A través de el atributo `inputWrapperStyle`", + }, + styleContainer: { + label: "Estilo del contenedor", + helper: "A través de el atributo `containerStyle`", + }, + styleLabel: { + label: "Estilo de la etiqueta y texto de ayuda", + helper: "A través de las props de estilo `LabelTextProps` y `HelperTextProps`", + }, + styleAccessories: { + label: "Estilo de los accesorios", + helper: "A través de las props de estilo `RightAccessory` y `LeftAccessory`", + }, + }, + }, + }, + demoToggle: { + description: + "Dibuja un switch de tipo booleano. Este componente requiere un callback `onValueChange` que actualice el atributo `value` para que este refleje las acciones del usuario. Si el atributo `value` no se actualiza, el componente seguirá mostrando el valor proporcionado por defecto en lugar de lo esperado por las acciones del usuario.", + useCase: { + variants: { + name: "Variantes", + description: + "El componente soporta diferentes variantes. Si se necesita una personalización más avanzada o variante específica, puede ser fácilmente refactorizada. El valor por defecto es `checkbox`.", + checkbox: { + label: "Variante `checkbox`", + helper: "Puede ser utilizada para un único valor del tipo on/off.", + }, + radio: { + label: "Variante `radio`", + helper: "Usa esto cuando tengas múltiples opciones.", + }, + switch: { + label: "Variante `switch`", + helper: + "Una entrada del tipo on/off que sobresale más. Tiene mejor soporte de accesibilidad.", + }, + }, + statuses: { + name: "Estados", + description: + "Hay un atributo de estado - similar a `preset` en otros componentes, pero que además impacta en la funcionalidad del componente.", + noStatus: "Sin estado - este es el valor por defecto", + errorStatus: "Estado de error - para usar cuando haya un error", + disabledStatus: "Estado desactivado - desactiva la edición y silencia el input", + }, + passingContent: { + name: "Entregando contenido", + description: "Hay varias formas de entregar contenido.", + useCase: { + checkBox: { + label: "A través del atributo `labelTx`", + helper: "A través del atributo `helperTx`.", + }, + checkBoxMultiLine: { + helper: "Soporta multi líneas - Nulla proident consectetur labore sunt ea labore.", + }, + radioChangeSides: { + helper: "Puedes cambiarle el lado - Laborum labore adipisicing in eu ipsum deserunt.", + }, + customCheckBox: { + label: "Pasa un ícono para un checkbox personalizado.", + }, + switch: { + label: "Los interruptores pueden leerse como texto", + helper: + "Por defecto, esta opción no usa `Text` ya que, dependiendo de la fuente, los caracteres on/off podrían no dibujarse bien. Personalízalo según tus necesidades.", + }, + switchAid: { + label: "O con la ayuda de un ícono", + }, + }, + }, + styling: { + name: "Estilo", + description: "El componente puede ser configurado fácilmente.", + outerWrapper: "1 - configura el contenedor externo del input", + innerWrapper: "2 - configura el contenedor interno del input", + inputDetail: "3 - configura el detalle del input", + labelTx: "También puedes configurar el atributo labelTx", + styleContainer: "O, configura todo el contenedor", + }, + }, + }, + demoButton: { + description: + "Un componente que permite a los usuarios realizar acciones y hacer elecciones. Rodea un componente Text con otro componente Pressable.", + useCase: { + presets: { + name: "Preajustes", + description: "Hay algunos preajustes por defecto.", + }, + passingContent: { + name: "Entregando contenido", + description: "Hay varias formas de entregar contenido.", + viaTextProps: "A través del atributo `text` - Billum In", + children: "Contenido anidado (children) - Irure Reprehenderit", + rightAccessory: "Componente derecho - Duis Quis", + leftAccessory: "Componente izquierdo - Duis Proident", + nestedChildren: "Contenido anidado - proident veniam.", + nestedChildren2: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + nestedChildren3: "Occaecat aliqua irure proident veniam.", + multiLine: + "Multilínea - consequat veniam veniam reprehenderit. Fugiat id nisi quis duis sunt proident mollit dolor mollit adipisicing proident deserunt.", + }, + styling: { + name: "Estilo", + description: "El componente puede ser configurando fácilmente.", + styleContainer: "Estilo del contenedor - Exercitation", + styleText: "Estilo del texto - Ea Anim", + styleAccessories: "Estilo de los componentes - enim ea id fugiat anim ad.", + pressedState: "Estilo para el estado presionado - fugiat anim", + }, + disabling: { + name: "Desactivado", + description: + "El componente puede ser desactivado y como consecuencia, estilizado. El comportamiento para hacer clic será desactivado.", + standard: "Desactivado - estándar", + filled: "Desactivado - relleno", + reversed: "Desactivado - invertido", + accessory: "Estilo del componente desactivado", + textStyle: "Estilo del texto desactivado", + }, + }, + }, + demoListItem: { + description: + "Un componente estilizado que representa una fila para ser utilizada dentro de un FlatList, SectionList o por sí solo.", + useCase: { + height: { + name: "Altura", + description: "La fila puede tener diferentes alturas.", + defaultHeight: "Altura por defecto (56px)", + customHeight: "Altura personalizada a través del atributo `height`", + textHeight: + "Altura determinada por el contenido del texto - Reprehenderit incididunt deserunt do do ea labore.", + longText: + "Limitar texto largo a solo una línea - Reprehenderit incididunt deserunt do do ea labore.", + }, + separators: { + name: "Separadores", + description: "El separador/divisor está preconfigurado y es opcional.", + topSeparator: "Separador solo en la parte superior", + topAndBottomSeparator: "Separadores en la parte superior e inferior", + bottomSeparator: "Separador solo en la parte inferior", + }, + icons: { + name: "Íconos", + description: "Puedes personalizar los íconos a la izquierda o a la derecha.", + leftIcon: "Ícono izquierdo", + rightIcon: "Ícono derecho", + leftRightIcons: "Íconos izquierdo y derecho", + }, + customLeftRight: { + name: "Componentes personalizados en la izquierda o derecha", + description: + "Puede pasar un componente personalizado en la izquierda o derecha, si así lo necesitas.", + customLeft: "Componente personalizado a la izquierda", + customRight: "Componente personalizado a la derecha", + }, + passingContent: { + name: "Entregando contenido", + description: "Hay varias formas de entregar contenido.", + text: "A través del atributo `text` - reprehenderit sint", + children: "Contenido anidado (children) - mostrud mollit", + nestedChildren1: "Contenido anidado 1 - proident veniam.", + nestedChildren2: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + }, + listIntegration: { + name: "Integración con FlatList y FlashList", + description: + "El componente puede ser fácilmente integrado con tu interfaz de lista preferida.", + }, + styling: { + name: "Estilo", + description: "El componente puede ser configurando fácilmente.", + styledText: "Texto estilizado", + styledContainer: "Contenedor estilizado (separadores)", + tintedIcons: "Íconos coloreados", + }, + }, + }, + demoCard: { + description: + "Las tarjetas son útiles para mostrar información relacionada de forma englobada. Si un ListItem muestra el contenido horizontalmente, una tarjeta puede ser también utilizada para mostrar el contenido de manera vertical.", + useCase: { + presets: { + name: "Preajustes", + description: "Hay algunos ajustes preconfigurados.", + default: { + heading: "Preajuste por defecto (default)", + content: "Incididunt magna ut aliquip consectetur mollit dolor.", + footer: "Consectetur nulla non aliquip velit.", + }, + reversed: { + heading: "Preajuste inverso", + content: "Reprehenderit occaecat proident amet id laboris.", + footer: "Consectetur tempor ea non labore anim.", + }, + }, + verticalAlignment: { + name: "Alineamiento vertical", + description: + "Dependiendo del requerimiento, la tarjeta está preconfigurada con diferentes estrategias de alineación.", + top: { + heading: "Arriba (por defecto)", + content: "Todo el contenido está automáticamente alineado en la parte superior.", + footer: "Incluso en el pie de página", + }, + center: { + heading: "Centro", + content: "El contenido está centrado en relación con la altura de la tarjeta.", + footer: "¡Yo también!", + }, + spaceBetween: { + heading: "Espacio entre", + content: "Todo el contenido está espaciado uniformemente.", + footer: "Estoy donde quiero estar.", + }, + reversed: { + heading: "Forzar el pie de página hacia abajo", + content: "Esto empuja el pie de página hacia donde pertenece.", + footer: "Estoy tan solo aquí abajo.", + }, + }, + passingContent: { + name: "Entregando contenido", + description: "Hay varias formas de entregar contenido.", + heading: "A través del atributo `heading`", + content: "A través del atributo `content`", + footer: "Estoy tan solo aquí abajo.", + }, + customComponent: { + name: "Componentes personalizados", + description: + "Cualquier componente preconfigurado puede ser reemplazado por uno específico. Puedes agregar otros si así lo requieres.", + rightComponent: "Componente derecho", + leftComponent: "Componente izquierdo", + }, + style: { + name: "Estilo", + description: "El componente puede ser configurado fácilmente.", + heading: "Estilizar el encabezado", + content: "Estilizar el contenido", + footer: "Estilizar el pie de página", + }, + }, + }, + demoAutoImage: { + description: + "Un componente que se ajusta automáticamente el tamaño de una imagen remota o utilizando el atributo data-uri.", + useCase: { + remoteUri: { name: "URI remota" }, + base64Uri: { name: "URI Base64" }, + scaledToFitDimensions: { + name: "Escalado que se ajusta a las dimensiones", + description: + "Al proporcionar los atributos `maxWidth` y/o `maxHeight`, la imagen se redimensionará automáticamente manteniendo el ratio. ¿En qué se diferencia de `resizeMode: 'contain'`? Para empezar, puedes especificar el tamaño de un solo lado (no ambos). Segundo, la imagen se ajustará a las dimensiones deseadas en lugar de simplemente estar contenida en su contenedor.", + heightAuto: "ancho: 60 / altura: auto", + widthAuto: "ancho: auto / altura: 32", + bothManual: "ancho: 60 / altura: 60", + }, + }, + }, + demoText: { + description: + "Para todo tipo de requerimiento relacionado a mostrar texto. Este componente es un 'wrapper' (HOC) del componente Text de React Native.", + useCase: { + presets: { + name: "Preajustes", + description: "Hay algunos ajustes preconfigurados.", + default: + "ajuste por defecto - Cillum eu laboris in labore. Excepteur mollit tempor reprehenderit fugiat elit et eu consequat laborum.", + bold: "preajuste negrita - Tempor et ullamco cupidatat in officia. Nulla ea duis elit id sunt ipsum cillum duis deserunt nostrud ut nostrud id.", + subheading: "preajuste subtítulo - In Cupidatat Cillum.", + heading: "preajuste título - Voluptate Adipis.", + }, + sizes: { + name: "Tamaños", + description: "Hay un atributo de tamaño.", + xs: "xs - Ea ipsum est ea ex sunt.", + sm: "sm - Lorem sunt adipisicin.", + md: "md - Consequat id do lorem.", + lg: "lg - Nostrud ipsum ea.", + xl: "xl - Eiusmod ex excepteur.", + xxl: "xxl - Cillum eu laboris.", + }, + weights: { + name: "Grueso", + description: "Hay un atributo de grueso.", + light: + "ligero - Nulla magna incididunt excepteur est occaecat duis culpa dolore cupidatat enim et.", + normal: + "normal - Magna incididunt dolor ut veniam veniam laboris aliqua velit ea incididunt.", + medium: "medio - Non duis laborum quis laboris occaecat culpa cillum.", + semibold: "seminegrita - Exercitation magna nostrud pariatur laborum occaecat aliqua.", + bold: "negrita - Eiusmod ullamco magna exercitation est excepteur.", + }, + passingContent: { + name: "Entregando contenido", + description: "Hay varias formas de entregar contenido.", + viaText: + "a través del atributo `text` - Billum in aute fugiat proident nisi pariatur est. Cupidatat anim cillum eiusmod ad. Officia eu magna aliquip labore dolore consequat.", + viaTx: "a través del atributo `tx` -", + children: + "Contenido anidado (children) - Aliqua velit irure reprehenderit eu qui amet veniam consectetur.", + nestedChildren: "Contenidos anidados -", + nestedChildren2: "Occaecat aliqua irure proident veniam.", + nestedChildren3: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + nestedChildren4: "Occaecat aliqua irure proident veniam.", + }, + styling: { + name: "Estilo", + description: "El componente puede ser configurando fácilmente.", + text: "Consequat ullamco veniam velit mollit proident excepteur aliquip id culpa ipsum velit sint nostrud.", + text2: + "Eiusmod occaecat laboris eu ex veniam ipsum adipisicing consectetur. Magna ullamco adipisicing tempor adipisicing.", + text3: + "Eiusmod occaecat laboris eu ex veniam ipsum adipisicing consectetur. Magna ullamco adipisicing tempor adipisicing.", + }, + }, + }, + demoHeader: { + description: + "Componente desplegado en varias pantallas. Va a contener botones de navegación y el título de la pantalla.", + useCase: { + actionIcons: { + name: "Íconos de acción", + description: "Puedes pasar fácilmente íconos a los componentes de la izquierda o derecha.", + leftIconTitle: "Ícono izquierdo", + rightIconTitle: "Ícono derecho", + bothIconsTitle: "Ambos íconos", + }, + actionText: { + name: "Texto de acción", + description: "Puedes pasar fácilmente texto a los componentes de la izquierda o derecha.", + leftTxTitle: "A través de `leftTx`", + rightTextTitle: "A través de `rightText`", + }, + customActionComponents: { + name: "Componentes personalizados de acción", + description: + "Si las opciones de ícono o texto no son suficientes, puedes pasar tu propio componente personalizado de acción.", + customLeftActionTitle: "Acción izquierda personalizada", + }, + titleModes: { + name: "Alineamiento para el título", + description: + "El título puede ser forzado a permanecer centrado (por defecto), pero podría cortarse si es demasiado largo. También puedes hacer que se ajuste a los botones a la izquierda o derecha.", + centeredTitle: "Título centrado", + flexTitle: "Título flexible", + }, + styling: { + name: "Estilo", + description: "El componente puede ser configurado fácilmente.", + styledTitle: "Título estilizado", + styledWrapperTitle: "Contenedor estilizado", + tintedIconsTitle: "Íconos coloreados", + }, + }, + }, + demoEmptyState: { + description: + "Un componente para cuando no hay información que mostrar. Puede usarse también para guiar al usuario sobre qué hacer a continuación.", + useCase: { + presets: { + name: "Preajustes", + description: + "Puedes crear distintos conjuntos de texto/imagen. Por ejemplo, con un ajuste predefinido `generic`. Si quieres tener un EmptyState completamente personalizado, ten en cuenta que no hay un valor por defecto.", + }, + passingContent: { + name: "Entregando contenido", + description: "Hay varias formas de entregar contenido.", + customizeImageHeading: "Personalizar la imagen", + customizeImageContent: "Puedes pasar cualquier una imagen de distintas fuentes.", + viaHeadingProp: "A través del atributo `heading`", + viaContentProp: "A través del atributo `content`.", + viaButtonProp: "A través del atributo `button`", + }, + styling: { + name: "Estilo", + description: "El componente puede ser configurado fácilmente.", + }, + }, + }, +} + +export default demoEs diff --git a/app/i18n/demo-fr.ts b/app/i18n/demo-fr.ts new file mode 100644 index 00000000..bca8ede4 --- /dev/null +++ b/app/i18n/demo-fr.ts @@ -0,0 +1,469 @@ +import { DemoTranslations } from "./demo-en" + +export const demoFr: DemoTranslations = { + demoIcon: { + description: + "Un composant pour faire le rendu d’une icône enregistrée. Il est enveloppé dans un si `onPress` est fourni, sinon dans une .", + useCase: { + icons: { + name: "Icônes", + description: "Liste des icônes enregistrées dans le composant.", + }, + size: { + name: "Taille", + description: "Il y a une prop de taille.", + }, + color: { + name: "Couleur", + description: "Il y a une prop de couleur.", + }, + styling: { + name: "Style", + description: "Le composant peut être facilement stylisé.", + }, + }, + }, + demoTextField: { + description: "Le composant permet la saisie et l'édition de texte.", + useCase: { + statuses: { + name: "Statuts", + description: + "Il y a une prop de statut - similaire à `preset` dans d'autres composants, mais affecte également la fonctionnalité du composant.", + noStatus: { + label: "Pas de statut", + helper: "C'est le statut par défaut", + placeholder: "Le texte passe par là", + }, + error: { + label: "Statut d'erreur", + helper: "Statut à utiliser en cas d’erreur", + placeholder: "Le texte passe par ici", + }, + disabled: { + label: "Statut désactivé", + helper: "Désactive l’édition et atténue le texte", + placeholder: "Le texte repasse par là", + }, + }, + passingContent: { + name: "Transfert de contenu", + description: "Il y a plusieurs façons de transmettre du contenu.", + viaLabel: { + labelTx: "Via la prop `label`", + helper: "Via la prop `helper`", + placeholder: "Via la prop `placeholder`", + }, + rightAccessory: { + label: "Accessoire droit", + helper: "Cette prop demande une fonction qui retourne un élément React.", + }, + leftAccessory: { + label: "Accessoire gauche", + helper: "Cette prop demande une fonction qui retourne un élément React.", + }, + supportsMultiline: { + label: "Supporte le multiligne", + helper: "Permet une saisie plus longue pour le texte multiligne.", + }, + }, + styling: { + name: "Style", + description: "Le composant peut être facilement stylisé.", + styleInput: { + label: "Style de saisie", + helper: "Via la prop `style`", + }, + styleInputWrapper: { + label: "Style du wrapper de saisie", + helper: "Via la prop `inputWrapperStyle`", + }, + styleContainer: { + label: "Style du conteneur", + helper: "Via la prop `containerStyle`", + }, + styleLabel: { + label: "Style du label et de l’aide", + helper: "Via les props de style `LabelTextProps` et `HelperTextProps`", + }, + styleAccessories: { + label: "Style des accessoires", + helper: "Via les props de style `RightAccessory` et `LeftAccessory`", + }, + }, + }, + }, + demoToggle: { + description: + "Fait le rendu d’un booléen. Ce composant contrôlé nécessite un callback `onValueChange` qui met à jour la prop `value` pour que le composant reflète les actions de l'utilisateur. Si la prop `value` n'est pas mise à jour, le composant continuera à rendre la prop `value` fournie au lieu du résultat attendu des actions de l'utilisateur.", + useCase: { + variants: { + name: "Variantes", + description: + "Le composant supporte différentes variantes. Si une personnalisation poussée d'une variante spécifique est nécessaire, elle peut être facilement refactorisée. La valeur par défaut est `checkbox`.", + checkbox: { + label: "Variante `checkbox`", + helper: "Peut être utilisée pour une seule valeure on/off.", + }, + radio: { + label: "Variante `radio`", + helper: "Utilisez ceci quand vous avez plusieurs options.", + }, + switch: { + label: "Variante `switch`", + helper: + "Une entrée on/off plus proéminente. Possède un meilleur support d’accessibilité.", + }, + }, + statuses: { + name: "Statuts", + description: + "Il y a une prop de statut - similaire à `preset` dans d'autres composants, mais affecte également la fonctionnalité du composant.", + noStatus: "Pas de statut - c'est le défaut", + errorStatus: "Statut d’erreur - à utiliser quand il y a une erreur", + disabledStatus: "Statut désactivé - désactive l’édition et atténue le style", + }, + passingContent: { + name: "Transfert de contenu", + description: "Il y a plusieurs façons de transmettre du contenu.", + useCase: { + checkBox: { + label: "Via la prop `labelTx`", + helper: "Via la prop `helperTx`.", + }, + checkBoxMultiLine: { + helper: "Supporte le multiligne - Nulla proident consectetur labore sunt ea labore. ", + }, + radioChangeSides: { + helper: + "Vous pouvez changer de côté - Laborum labore adipisicing in eu ipsum deserunt.", + }, + customCheckBox: { + label: "Passez une icône de case à cocher personnalisée.", + }, + switch: { + label: "Les interrupteurs peuvent être lus comme du texte", + helper: + "Par défaut, cette option n’utilise pas `Text` car selon la police, les caractères on/off pourraient paraître étranges. Personnalisez selon vos besoins.", + }, + switchAid: { + label: "Ou aidé d’une icône", + }, + }, + }, + styling: { + name: "Style", + description: "Le composant peut être facilement stylisé.", + outerWrapper: "1 - styliser le wrapper extérieur de l’entrée", + innerWrapper: "2 - styliser le wrapper intérieur de l’entrée", + inputDetail: "3 - styliser le détail de l’entrée", + labelTx: "Vous pouvez aussi styliser le labelTx", + styleContainer: "Ou, styliser le conteneur entier", + }, + }, + }, + demoButton: { + description: + "Un composant qui permet aux utilisateurs d’effectuer des actions et de faire des choix. Enveloppe le composant Text avec un composant Pressable.", + useCase: { + presets: { + name: "Préréglages", + description: "Il y a quelques préréglages préconfigurés.", + }, + passingContent: { + name: "Transfert de contenu", + description: "Il y a plusieurs façons de transmettre du contenu.", + viaTextProps: "Via la prop `text` - Billum In", + children: "Enfants - Irure Reprehenderit", + rightAccessory: "Accessoire droit - Duis Quis", + leftAccessory: "Accessoire gauche - Duis Proident", + nestedChildren: "Enfants imbriqués - proident veniam.", + nestedChildren2: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + nestedChildren3: "Occaecat aliqua irure proident veniam.", + multiLine: + "Multiligne - consequat veniam veniam reprehenderit. Fugiat id nisi quis duis sunt proident mollit dolor mollit adipisicing proident deserunt.", + }, + styling: { + name: "Style", + description: "Le composant peut être facilement stylisé.", + styleContainer: "Style du conteneur - Exercitation", + styleText: "Style du texte - Ea Anim", + styleAccessories: "Style des accessoires - enim ea id fugiat anim ad.", + pressedState: "Style de l’état pressé - fugiat anim", + }, + disabling: { + name: "Désactivation", + description: + "Le composant peut être désactivé et stylisé en conséquence. Le comportement de pression sera désactivé.", + standard: "Désactivé - standard", + filled: "Désactivé - rempli", + reversed: "Désactivé - inversé", + accessory: "Style d’accessoire désactivé", + textStyle: "Style de texte désactivé", + }, + }, + }, + demoListItem: { + description: + "Un composant de ligne stylisé qui peut être utilisé dans FlatList, SectionList, ou seul.", + useCase: { + height: { + name: "Hauteur", + description: "La ligne peut avoir différentes hauteurs.", + defaultHeight: "Hauteur par défaut (56px)", + customHeight: "Hauteur personnalisée via la prop `height`", + textHeight: + "Hauteur déterminée par le contenu du texte - Reprehenderit incididunt deserunt do do ea labore.", + longText: + "Limiter le texte long à une ligne - Reprehenderit incididunt deserunt do do ea labore.", + }, + separators: { + name: "Séparateurs", + description: "Le séparateur / diviseur est préconfiguré et optionnel.", + topSeparator: "Séparateur uniquement en haut", + topAndBottomSeparator: "Séparateurs en haut et en bas", + bottomSeparator: "Séparateur uniquement en bas", + }, + icons: { + name: "Icônes", + description: "Vous pouvez personnaliser les icônes à gauche ou à droite.", + leftIcon: "Icône gauche", + rightIcon: "Icône droite", + leftRightIcons: "Icônes gauche et droite", + }, + customLeftRight: { + name: "Composants personnalisés gauche/droite", + description: + "Si vous avez besoin d’un composant personnalisé à gauche/droite, vous pouvez le passer.", + customLeft: "Composant personnalisé à gauche", + customRight: "Composant personnalisé à droite", + }, + passingContent: { + name: "Transfert de contenu", + description: "Il y a plusieurs façons de transmettre du contenu.", + text: "Via la prop `text` - reprehenderit sint", + children: "Enfants - mostrud mollit", + nestedChildren1: "Enfants imbriqués - proident veniam.", + nestedChildren2: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + }, + listIntegration: { + name: "Intégration avec FlatList & FlashList", + description: + "Le composant peut être facilement intégré avec votre interface de liste préférée.", + }, + styling: { + name: "Style", + description: "Le composant peut être facilement stylisé.", + styledText: "Texte stylisé", + styledContainer: "Conteneur stylisé (séparateurs)", + tintedIcons: "Icônes teintées", + }, + }, + }, + demoCard: { + description: + "Les cartes sont utiles pour afficher des informations connexes de manière contenue. Si un ListItem affiche le contenu horizontalement, une Card peut être utilisée pour afficher le contenu verticalement.", + useCase: { + presets: { + name: "Préréglages", + description: "Il y a quelques préréglages préconfigurés.", + default: { + heading: "Préréglage par défaut (default)", + content: "Incididunt magna ut aliquip consectetur mollit dolor.", + footer: "Consectetur nulla non aliquip velit.", + }, + reversed: { + heading: "Préréglage inversé", + content: "Reprehenderit occaecat proident amet id laboris.", + footer: "Consectetur tempor ea non labore anim .", + }, + }, + verticalAlignment: { + name: "Alignement vertical", + description: + "Selon les besoins, la carte est préconfigurée avec différentes stratégies d’alignement.", + top: { + heading: "Haut (par défaut)", + content: "Tout le contenu est automatiquement aligné en haut.", + footer: "Même le pied de page", + }, + center: { + heading: "Centre", + content: "Le contenu est centré par rapport à la hauteur de la carte.", + footer: "Moi aussi !", + }, + spaceBetween: { + heading: "Espace entre", + content: "Tout le contenu est espacé uniformément.", + footer: "Je suis là où je veux être.", + }, + reversed: { + heading: "Forcer le pied de page en bas", + content: "Cela pousse le pied de page là où il appartient.", + footer: "Je suis si seul ici en bas.", + }, + }, + passingContent: { + name: "Transfert de contenu", + description: "Il y a plusieurs façons de transmettre du contenu.", + heading: "Via la prop `heading`", + content: "Via la prop `content`", + footer: "Je suis si seul ici en bas.", + }, + customComponent: { + name: "Composants personnalisés", + description: + "N’importe quels composants préconfigurés peuvent être remplacé par le vôtre. Vous pouvez également en ajouter d’autres.", + rightComponent: "Composant droit", + leftComponent: "Composant gauche", + }, + style: { + name: "Style", + description: "Le composant peut être facilement stylisé.", + heading: "Styliser l’en-tête", + content: "Styliser le contenu", + footer: "Styliser le pied de page", + }, + }, + }, + demoAutoImage: { + description: + "Un composant Image qui dimensionne automatiquement une image distante ou data-uri.", + useCase: { + remoteUri: { name: "URI distante" }, + base64Uri: { name: "URI Base64" }, + scaledToFitDimensions: { + name: "Mis à l’échelle pour s’adapter aux dimensions", + description: + "En fournissant les props `maxWidth` et/ou `maxHeight`, l’image se redimensionnera automatiquement à l’échelle tout en conservant son rapport d’aspect. En quoi est-ce différent de `resizeMode: 'contain'` ? Premièrement, vous pouvez spécifier la taille d'un seul côté (pas les deux). Deuxièmement, l'image s'adaptera aux dimensions souhaitées au lieu d'être simplement contenue dans son conteneur d'image.", + heightAuto: "largeur: 60 / hauteur: auto", + widthAuto: "largeur: auto / hauteur: 32", + bothManual: "largeur: 60 / hauteur: 60", + }, + }, + }, + demoText: { + description: + "Pour vos besoins d'affichage de texte. Ce composant est un HOC sur celui intégré à React Native.", + useCase: { + presets: { + name: "Préréglages", + description: "Il y a quelques réglages préconfigurés.", + default: + "préréglage par défaut - Cillum eu laboris in labore. Excepteur mollit tempor reprehenderit fugiat elit et eu consequat laborum.", + bold: "préréglage gras - Tempor et ullamco cupidatat in officia. Nulla ea duis elit id sunt ipsum cillum duis deserunt nostrud ut nostrud id.", + subheading: "préréglage sous-titre - In Cupidatat Cillum.", + heading: "préréglage titre - Voluptate Adipis.", + }, + sizes: { + name: "Tailles", + description: "Il y a une prop de taille.", + xs: "xs - Ea ipsum est ea ex sunt.", + sm: "sm - Lorem sunt adipisicin.", + md: "md - Consequat id do lorem.", + lg: "lg - Nostrud ipsum ea.", + xl: "xl - Eiusmod ex excepteur.", + xxl: "xxl - Cillum eu laboris.", + }, + weights: { + name: "Graisse", + description: "Il y a une prop de graisse.", + light: + "léger - Nulla magna incididunt excepteur est occaecat duis culpa dolore cupidatat enim et.", + normal: + "normal - Magna incididunt dolor ut veniam veniam laboris aliqua velit ea incididunt.", + medium: "moyen - Non duis laborum quis laboris occaecat culpa cillum.", + semibold: "demi-gras - Exercitation magna nostrud pariatur laborum occaecat aliqua.", + bold: "gras - Eiusmod ullamco magna exercitation est excepteur.", + }, + passingContent: { + name: "Transfert de contenu", + description: "Il y a plusieurs façons de transférer du contenu.", + viaText: + "via la prop `text` - Billum in aute fugiat proident nisi pariatur est. Cupidatat anim cillum eiusmod ad. Officia eu magna aliquip labore dolore consequat.", + viaTx: "via la prop `tx` -", + children: "enfants - Aliqua velit irure reprehenderit eu qui amet veniam consectetur.", + nestedChildren: "Enfants imbriqués -", + nestedChildren2: "Occaecat aliqua irure proident veniam.", + nestedChildren3: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + nestedChildren4: "Occaecat aliqua irure proident veniam.", + }, + styling: { + name: "Style", + description: "Le composant peut être facilement stylisé.", + text: "Consequat ullamco veniam velit mollit proident excepteur aliquip id culpa ipsum velit sint nostrud.", + text2: + "Eiusmod occaecat laboris eu ex veniam ipsum adipisicing consectetur. Magna ullamco adipisicing tempor adipisicing.", + text3: + "Eiusmod occaecat laboris eu ex veniam ipsum adipisicing consectetur. Magna ullamco adipisicing tempor adipisicing.", + }, + }, + }, + demoHeader: { + description: + "Composant qui apparaît sur de nombreux écrans. Contiendra les boutons de navigation et le titre de l’écran.", + useCase: { + actionIcons: { + name: "Icônes d’action", + description: + "Vous pouvez facilement passer des icônes aux composants d’action gauche ou droit.", + leftIconTitle: "Icône gauche", + rightIconTitle: "Icône droite", + bothIconsTitle: "Les deux icônes", + }, + actionText: { + name: "Texte d’action", + description: + "Vous pouvez facilement passer du texte aux composants d’action gauche ou droit.", + leftTxTitle: "Via `leftTx`", + rightTextTitle: "Via `rightText`", + }, + customActionComponents: { + name: "Composants d’action personnalisés", + description: + "Si les options d’icône ou de texte ne suffisent pas, vous pouvez passer votre propre composant d’action personnalisé.", + customLeftActionTitle: "Action gauche personnalisée", + }, + titleModes: { + name: "Modes de titre", + description: + "Le titre peut être forcé à rester au centre (par défaut) mais peut être coupé s’il est trop long. Vous pouvez éventuellement le faire s’ajuster aux boutons d’action.", + centeredTitle: "Titre centré", + flexTitle: "Titre flexible", + }, + styling: { + name: "Style", + description: "Le composant peut être facilement stylisé.", + styledTitle: "Titre stylisé", + styledWrapperTitle: "Wrapper stylisé", + tintedIconsTitle: "Icônes teintées", + }, + }, + }, + demoEmptyState: { + description: + "Un composant à utiliser lorsqu’il n’y a pas de données à afficher. Il peut être utilisé pour diriger l’utilisateur sur ce qu’il faut faire ensuite.", + useCase: { + presets: { + name: "Préréglages", + description: + "Vous pouvez créer différents ensembles de texte/image. Un est prédéfini appelé `generic`. Notez qu’il n’y a pas de valeur par défaut au cas où vous voudriez avoir un EmptyState complètement personnalisé.", + }, + passingContent: { + name: "Transfert de contenu", + description: "Il y a plusieurs façons de transférer du contenu.", + customizeImageHeading: "Personnaliser l’image", + customizeImageContent: "Vous pouvez passer n’importe quelle source d'image.", + viaHeadingProp: "Via la prop `heading`", + viaContentProp: "Via la prop `content`.", + viaButtonProp: "Via la prop `button`", + }, + styling: { + name: "Style", + description: "Le composant peut être facilement stylisé.", + }, + }, + }, +} + +export default demoFr diff --git a/app/i18n/demo-hi.ts b/app/i18n/demo-hi.ts new file mode 100644 index 00000000..f4a14cde --- /dev/null +++ b/app/i18n/demo-hi.ts @@ -0,0 +1,466 @@ +import { DemoTranslations } from "./demo-en" + +export const demoHi: DemoTranslations = { + demoIcon: { + description: + "एक पंजीकृत आइकन को रेंडर करने के लिए एक कंपोनेंट। यदि `onPress` प्रदान किया जाता है तो यह में लपेटा जाता है, अन्यथा में।", + useCase: { + icons: { + name: "आइकन", + description: "कंपोनेंट के अंदर पंजीकृत आइकनों की सूची।", + }, + size: { + name: "आकार", + description: "एक आकार प्रॉप है।", + }, + color: { + name: "रंग", + description: "एक रंग प्रॉप है।", + }, + styling: { + name: "स्टाइलिंग", + description: "कंपोनेंट को आसानी से स्टाइल किया जा सकता है।", + }, + }, + }, + demoTextField: { + description: "टेक्स्टफील्ड कंपोनेंट टेक्स्ट दर्ज करने और संपादित करने की अनुमति देता है।", + useCase: { + statuses: { + name: "स्थितियाँ", + description: + "एक स्थिति प्रॉप है - अन्य कंपोनेंट्स में `preset` के समान, लेकिन कंपोनेंट की कार्यक्षमता को भी प्रभावित करता है।", + noStatus: { + label: "कोई स्थिति नहीं", + helper: "यह डिफ़ॉल्ट स्थिति है", + placeholder: "टेक्स्ट यहाँ जाता है", + }, + error: { + label: "त्रुटि स्थिति", + helper: "त्रुटि होने पर उपयोग करने के लिए स्थिति", + placeholder: "टेक्स्ट यहाँ जाता है", + }, + disabled: { + label: "अक्षम स्थिति", + helper: "संपादन को अक्षम करता है और टेक्स्ट को मंद करता है", + placeholder: "टेक्स्ट यहाँ जाता है", + }, + }, + passingContent: { + name: "सामग्री पास करना", + description: "सामग्री पास करने के कई तरीके हैं।", + viaLabel: { + labelTx: "`label` प्रॉप के माध्यम से", + helper: "`helper` प्रॉप के माध्यम से", + placeholder: "`placeholder` प्रॉप के माध्यम से", + }, + rightAccessory: { + label: "दायाँ सहायक", + helper: "यह प्रॉप एक फ़ंक्शन लेता है जो एक React तत्व लौटाता है।", + }, + leftAccessory: { + label: "बायाँ सहायक", + helper: "यह प्रॉप एक फ़ंक्शन लेता है जो एक React तत्व लौटाता है।", + }, + supportsMultiline: { + label: "मल्टीलाइन का समर्थन करता है", + helper: "मल्टीलाइन टेक्स्ट के लिए एक लंबा इनपुट सक्षम करता है।", + }, + }, + styling: { + name: "स्टाइलिंग", + description: "कंपोनेंट को आसानी से स्टाइल किया जा सकता है।", + styleInput: { + label: "इनपुट स्टाइल", + helper: "`style` प्रॉप के माध्यम से", + }, + styleInputWrapper: { + label: "इनपुट रैपर स्टाइल", + helper: "`inputWrapperStyle` प्रॉप के माध्यम से", + }, + styleContainer: { + label: "कंटेनर स्टाइल", + helper: "`containerStyle` प्रॉप के माध्यम से", + }, + styleLabel: { + label: "लेबल और हेल्पर स्टाइल", + helper: "`LabelTextProps` और `HelperTextProps` स्टाइल प्रॉप के माध्यम से", + }, + styleAccessories: { + label: "सहायक स्टाइल", + helper: "`RightAccessory` और `LeftAccessory` स्टाइल प्रॉप के माध्यम से", + }, + }, + }, + }, + demoToggle: { + description: + "एक बूलियन इनपुट रेंडर करता है। यह एक नियंत्रित कंपोनेंट है जिसे उपयोगकर्ता क्रियाओं को दर्शाने के लिए value प्रॉप को अपडेट करने वाले onValueChange कॉलबैक की आवश्यकता होती है। यदि value प्रॉप अपडेट नहीं की जाती है, तो कंपोनेंट उपयोगकर्ता क्रियाओं के अपेक्षित परिणाम के बजाय आपूर्ति की गई value प्रॉप को रेंडर करना जारी रखेगा।", + useCase: { + variants: { + name: "विविधताएँ", + description: + "कंपोनेंट कुछ अलग-अलग विविधताओं का समर्थन करता है। यदि किसी विशिष्ट विविधता के भारी अनुकूलन की आवश्यकता है, तो इसे आसानी से पुनर्गठित किया जा सकता है। डिफ़ॉल्ट `checkbox` है।", + checkbox: { + label: "`checkbox` विविधता", + helper: "इसका उपयोग एकल चालू/बंद इनपुट के लिए किया जा सकता है।", + }, + radio: { + label: "`radio` विविधता", + helper: "जब आपके पास कई विकल्प हों तो इसका उपयोग करें।", + }, + switch: { + label: "`switch` विविधता", + helper: "एक अधिक प्रमुख चालू/बंद इनपुट। बेहतर पहुँच समर्थन है।", + }, + }, + statuses: { + name: "स्थितियाँ", + description: + "एक स्थिति प्रॉप है - अन्य कंपोनेंट्स में `preset` के समान, लेकिन कंपोनेंट की कार्यक्षमता को भी प्रभावित करता है।", + noStatus: "कोई स्थिति नहीं - यह डिफ़ॉल्ट है", + errorStatus: "त्रुटि स्थिति - जब कोई त्रुटि हो तो उपयोग करें", + disabledStatus: "अक्षम स्थिति - संपादन को अक्षम करता है और इनपुट को मंद करता है", + }, + passingContent: { + name: "सामग्री पास करना", + description: "सामग्री पास करने के कई तरीके हैं।", + useCase: { + checkBox: { + label: "`labelTx` प्रॉप के माध्यम से", + helper: "`helperTx` प्रॉप के माध्यम से।", + }, + checkBoxMultiLine: { + helper: + "मल्टीलाइन का समर्थन करता है - Nulla proident consectetur labore sunt ea labore. ", + }, + radioChangeSides: { + helper: "आप पक्ष बदल सकते हैं - Laborum labore adipisicing in eu ipsum deserunt.", + }, + customCheckBox: { + label: "एक कस्टम चेकबॉक्स आइकन पास करें।", + }, + switch: { + label: "स्विच को टेक्स्ट के रूप में पढ़ा जा सकता है", + helper: + "डिफ़ॉल्ट रूप से, यह विकल्प `Text` का उपयोग नहीं करता है क्योंकि फ़ॉन्ट के आधार पर, चालू/बंद अक्षर अजीब दिख सकते हैं। आवश्यकतानुसार अनुकूलित करें।", + }, + switchAid: { + label: "या एक आइकन की मदद से", + }, + }, + }, + styling: { + name: "स्टाइलिंग", + description: "कंपोनेंट को आसानी से स्टाइल किया जा सकता है।", + outerWrapper: "1 - इनपुट के बाहरी रैपर को स्टाइल करें", + innerWrapper: "2 - इनपुट के आंतरिक रैपर को स्टाइल करें", + inputDetail: "3 - इनपुट विवरण को स्टाइल करें", + labelTx: "आप labelTx को भी स्टाइल कर सकते हैं", + styleContainer: "या, पूरे कंटेनर को स्टाइल करें", + }, + }, + }, + demoButton: { + description: + "एक कंपोनेंट जो उपयोगकर्ताओं को कार्रवाई करने और विकल्प चुनने की अनुमति देता है। Text कंपोनेंट को Pressable कंपोनेंट के साथ लपेटता है।", + useCase: { + presets: { + name: "प्रीसेट", + description: "कुछ पूर्व-कॉन्फ़िगर किए गए प्रीसेट हैं।", + }, + passingContent: { + name: "सामग्री पास करना", + description: "सामग्री पास करने के कई तरीके हैं।", + viaTextProps: "`text` प्रॉप के माध्यम से - Billum In", + children: "चिल्ड्रन - Irure Reprehenderit", + rightAccessory: "दायां एक्सेसरी - Duis Quis", + leftAccessory: "बायां एक्सेसरी - Duis Proident", + nestedChildren: "नेस्टेड चिल्ड्रन - proident veniam.", + nestedChildren2: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + nestedChildren3: "Occaecat aliqua irure proident veniam.", + multiLine: + "मल्टीलाइन - consequat veniam veniam reprehenderit. Fugiat id nisi quis duis sunt proident mollit dolor mollit adipisicing proident deserunt.", + }, + styling: { + name: "स्टाइलिंग", + description: "कंपोनेंट को आसानी से स्टाइल किया जा सकता है।", + styleContainer: "कंटेनर स्टाइल - Exercitation", + styleText: "टेक्स्ट स्टाइल - Ea Anim", + styleAccessories: "एक्सेसरीज़ स्टाइल - enim ea id fugiat anim ad.", + pressedState: "दबाए गए स्थिति का स्टाइल - fugiat anim", + }, + disabling: { + name: "अक्षम करना", + description: + "कंपोनेंट को अक्षम किया जा सकता है और उसके आधार पर स्टाइल किया जा सकता है। दबाने का व्यवहार अक्षम हो जाएगा।", + standard: "अक्षम - मानक", + filled: "अक्षम - भरा हुआ", + reversed: "अक्षम - उलटा", + accessory: "अक्षम एक्सेसरी स्टाइल", + textStyle: "अक्षम टेक्स्ट स्टाइल", + }, + }, + }, + demoListItem: { + description: + "एक स्टाइल किया गया पंक्ति कंपोनेंट जो FlatList, SectionList, या अकेले उपयोग किया जा सकता है।", + useCase: { + height: { + name: "ऊँचाई", + description: "पंक्ति की विभिन्न ऊँचाइयाँ हो सकती हैं।", + defaultHeight: "डिफ़ॉल्ट ऊँचाई (56px)", + customHeight: "`height` प्रॉप के माध्यम से कस्टम ऊँचाई", + textHeight: + "टेक्स्ट सामग्री द्वारा निर्धारित ऊँचाई - Reprehenderit incididunt deserunt do do ea labore.", + longText: + "लंबे टेक्स्ट को एक पंक्ति तक सीमित करें - Reprehenderit incididunt deserunt do do ea labore.", + }, + separators: { + name: "विभाजक", + description: "विभाजक / डिवाइडर पूर्व-कॉन्फ़िगर किया गया है और वैकल्पिक है।", + topSeparator: "केवल ऊपरी विभाजक", + topAndBottomSeparator: "ऊपरी और निचले विभाजक", + bottomSeparator: "केवल निचला विभाजक", + }, + icons: { + name: "आइकन", + description: "आप बाएँ या दाएँ आइकन को कस्टमाइज़ कर सकते हैं।", + leftIcon: "बायाँ आइकन", + rightIcon: "दायाँ आइकन", + leftRightIcons: "बाएँ और दाएँ आइकन", + }, + customLeftRight: { + name: "कस्टम बायाँ/दायाँ कंपोनेंट", + description: + "यदि आपको कस्टम बायाँ/दायाँ कंपोनेंट की आवश्यकता है, तो आप इसे पास कर सकते हैं।", + customLeft: "कस्टम बायाँ कंपोनेंट", + customRight: "कस्टम दायाँ कंपोनेंट", + }, + passingContent: { + name: "सामग्री पास करना", + description: "सामग्री पास करने के कई तरीके हैं।", + text: "`text` प्रॉप के माध्यम से - reprehenderit sint", + children: "चिल्ड्रन - mostrud mollit", + nestedChildren1: "नेस्टेड चिल्ड्रन - proident veniam.", + nestedChildren2: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + }, + listIntegration: { + name: "FlatList और FlashList के साथ एकीकरण", + description: + "कंपोनेंट को आसानी से आपके पसंदीदा सूची इंटरफेस के साथ एकीकृत किया जा सकता है।", + }, + styling: { + name: "स्टाइलिंग", + description: "कंपोनेंट को आसानी से स्टाइल किया जा सकता है।", + styledText: "स्टाइल किया गया टेक्स्ट", + styledContainer: "स्टाइल किया गया कंटेनर (विभाजक)", + tintedIcons: "रंगीन आइकन", + }, + }, + }, + demoCard: { + description: + "कार्ड संबंधित जानकारी को एक संयमित तरीके से प्रदर्शित करने के लिए उपयोगी हैं। यदि एक ListItem सामग्री को क्षैतिज रूप से प्रदर्शित करता है, तो एक कार्ड का उपयोग सामग्री को लंबवत रूप से प्रदर्शित करने के लिए किया जा सकता है।", + useCase: { + presets: { + name: "प्रीसेट", + description: "कुछ पूर्व-कॉन्फ़िगर किए गए प्रीसेट हैं।", + default: { + heading: "डिफ़ॉल्ट प्रीसेट (डिफ़ॉल्ट)", + content: "Incididunt magna ut aliquip consectetur mollit dolor.", + footer: "Consectetur nulla non aliquip velit.", + }, + reversed: { + heading: "रिवर्स्ड प्रीसेट", + content: "Reprehenderit occaecat proident amet id laboris.", + footer: "Consectetur tempor ea non labore anim .", + }, + }, + verticalAlignment: { + name: "ऊर्ध्वाधर संरेखण", + description: + "आवश्यकता के अनुसार, कार्ड विभिन्न संरेखण रणनीतियों के साथ पूर्व-कॉन्फ़िगर किया गया है।", + top: { + heading: "शीर्ष (डिफ़ॉल्ट)", + content: "सभी सामग्री स्वचालित रूप से शीर्ष पर संरेखित होती है।", + footer: "यहां तक कि फुटर भी", + }, + center: { + heading: "मध्य", + content: "सामग्री कार्ड की ऊंचाई के सापेक्ष केंद्रित होती है।", + footer: "मैं भी!", + }, + spaceBetween: { + heading: "स्पेस बिटवीन", + content: "सभी सामग्री समान रूप से फैली हुई है।", + footer: "मैं वहां हूं जहां मैं होना चाहता हूं।", + }, + reversed: { + heading: "फुटर को नीचे रखें", + content: "यह फुटर को उसके सही स्थान पर धकेलता है।", + footer: "मैं यहां नीचे बहुत अकेला हूं।", + }, + }, + passingContent: { + name: "सामग्री पास करना", + description: "सामग्री पास करने के कई तरीके हैं।", + heading: "`heading` प्रॉप के माध्यम से", + content: "`content` प्रॉप के माध्यम से", + footer: "मैं यहां नीचे बहुत अकेला हूं।", + }, + customComponent: { + name: "कस्टम कंपोनेंट्स", + description: + "किसी भी पूर्व-कॉन्फ़िगर किए गए कंपोनेंट को आपके अपने कंपोनेंट से बदला जा सकता है। आप अतिरिक्त कंपोनेंट भी जोड़ सकते हैं।", + rightComponent: "दायाँ कंपोनेंट", + leftComponent: "बायाँ कंपोनेंट", + }, + style: { + name: "स्टाइलिंग", + description: "कंपोनेंट को आसानी से स्टाइल किया जा सकता है।", + heading: "शीर्षक को स्टाइल करें", + content: "सामग्री को स्टाइल करें", + footer: "फुटर को स्टाइल करें", + }, + }, + }, + demoAutoImage: { + description: + "एक छवि कंपोनेंट जो स्वचालित रूप से रिमोट या डेटा-यूआरआई छवि का आकार निर्धारित करता है।", + useCase: { + remoteUri: { name: "रिमोट यूआरआई" }, + base64Uri: { name: "बेस64 यूआरआई" }, + scaledToFitDimensions: { + name: "आयामों के अनुरूप स्केल किया गया", + description: + "`maxWidth` और/या `maxHeight` प्रॉप्स प्रदान करने पर, छवि स्वचालित रूप से अपने आस्पेक्ट अनुपात को बनाए रखते हुए स्केल होगी। यह `resizeMode: 'contain'` से कैसे अलग है? पहला, आप केवल एक तरफ का आकार निर्दिष्ट कर सकते हैं (दोनों नहीं)। दूसरा, छवि वांछित आयामों के अनुरूप स्केल होगी, न कि केवल अपने छवि-कंटेनर के भीतर समाहित होगी।", + heightAuto: "चौड़ाई: 60 / ऊंचाई: स्वचालित", + widthAuto: "चौड़ाई: स्वचालित / ऊंचाई: 32", + bothManual: "चौड़ाई: 60 / ऊंचाई: 60", + }, + }, + }, + demoText: { + description: + "आपकी टेक्स्ट प्रदर्शन आवश्यकताओं के लिए। यह कंपोनेंट अंतर्निहित React Native कंपोनेंट पर एक HOC है।", + useCase: { + presets: { + name: "प्रीसेट", + description: "कुछ पूर्व-कॉन्फ़िगर किए गए प्रीसेट हैं।", + default: + "डिफ़ॉल्ट प्रीसेट - Cillum eu laboris in labore. Excepteur mollit tempor reprehenderit fugiat elit et eu consequat laborum.", + bold: "बोल्ड प्रीसेट - Tempor et ullamco cupidatat in officia. Nulla ea duis elit id sunt ipsum cillum duis deserunt nostrud ut nostrud id.", + subheading: "सबहेडिंग प्रीसेट - In Cupidatat Cillum.", + heading: "हेडिंग प्रीसेट - Voluptate Adipis.", + }, + sizes: { + name: "आकार", + description: "एक आकार प्रॉप है।", + xs: "xs - Ea ipsum est ea ex sunt.", + sm: "sm - Lorem sunt adipisicin.", + md: "md - Consequat id do lorem.", + lg: "lg - Nostrud ipsum ea.", + xl: "xl - Eiusmod ex excepteur.", + xxl: "xxl - Cillum eu laboris.", + }, + weights: { + name: "वजन", + description: "एक वजन प्रॉप है।", + light: + "लाइट - Nulla magna incididunt excepteur est occaecat duis culpa dolore cupidatat enim et.", + normal: + "सामान्य - Magna incididunt dolor ut veniam veniam laboris aliqua velit ea incididunt.", + medium: "मध्यम - Non duis laborum quis laboris occaecat culpa cillum.", + semibold: "सेमीबोल्ड - Exercitation magna nostrud pariatur laborum occaecat aliqua.", + bold: "बोल्ड - Eiusmod ullamco magna exercitation est excepteur.", + }, + passingContent: { + name: "सामग्री पास करना", + description: "सामग्री पास करने के कई तरीके हैं।", + viaText: + "`text` प्रॉप के माध्यम से - Billum in aute fugiat proident nisi pariatur est. Cupidatat anim cillum eiusmod ad. Officia eu magna aliquip labore dolore consequat.", + viaTx: "`tx` प्रॉप के माध्यम से -", + children: "चिल्ड्रन - Aliqua velit irure reprehenderit eu qui amet veniam consectetur.", + nestedChildren: "नेस्टेड चिल्ड्रन -", + nestedChildren2: "Occaecat aliqua irure proident veniam.", + nestedChildren3: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + nestedChildren4: "Occaecat aliqua irure proident veniam.", + }, + styling: { + name: "स्टाइलिंग", + description: "कंपोनेंट को आसानी से स्टाइल किया जा सकता है।", + text: "Consequat ullamco veniam velit mollit proident excepteur aliquip id culpa ipsum velit sint nostrud.", + text2: + "Eiusmod occaecat laboris eu ex veniam ipsum adipisicing consectetur. Magna ullamco adipisicing tempor adipisicing.", + text3: + "Eiusmod occaecat laboris eu ex veniam ipsum adipisicing consectetur. Magna ullamco adipisicing tempor adipisicing.", + }, + }, + }, + demoHeader: { + description: + "कई स्क्रीन पर दिखाई देने वाला कंपोनेंट। यह नेविगेशन बटन और स्क्रीन शीर्षक धारण करेगा।", + useCase: { + actionIcons: { + name: "एक्शन आइकन", + description: "आप आसानी से बाएँ या दाएँ एक्शन कंपोनेंट्स में आइकन पास कर सकते हैं।", + leftIconTitle: "बायाँ आइकन", + rightIconTitle: "दायाँ आइकन", + bothIconsTitle: "दोनों आइकन", + }, + actionText: { + name: "एक्शन टेक्स्ट", + description: "आप आसानी से बाएँ या दाएँ एक्शन कंपोनेंट्स में टेक्स्ट पास कर सकते हैं।", + leftTxTitle: "`leftTx` के माध्यम से", + rightTextTitle: "`rightText` के माध्यम से", + }, + customActionComponents: { + name: "कस्टम एक्शन कंपोनेंट्स", + description: + "यदि आइकन या टेक्स्ट विकल्प पर्याप्त नहीं हैं, तो आप अपना खुद का कस्टम एक्शन कंपोनेंट पास कर सकते हैं।", + customLeftActionTitle: "कस्टम बायाँ एक्शन", + }, + titleModes: { + name: "शीर्षक मोड", + description: + "शीर्षक को मध्य में रहने के लिए मजबूर किया जा सकता है (डिफ़ॉल्ट) लेकिन यदि यह बहुत लंबा है तो काटा जा सकता है। वैकल्पिक रूप से आप इसे एक्शन बटनों के अनुसार समायोजित कर सकते हैं।", + centeredTitle: "केंद्रित शीर्षक", + flexTitle: "फ्लेक्स शीर्षक", + }, + styling: { + name: "स्टाइलिंग", + description: "कंपोनेंट को आसानी से स्टाइल किया जा सकता है।", + styledTitle: "स्टाइल किया गया शीर्षक", + styledWrapperTitle: "स्टाइल किया गया रैपर", + tintedIconsTitle: "रंगीन आइकन", + }, + }, + }, + demoEmptyState: { + description: + "जब प्रदर्शित करने के लिए कोई डेटा नहीं है तो उपयोग करने के लिए एक कंपोनेंट। इसका उपयोग उपयोगकर्ता को अगला क्या करना है, यह निर्देशित करने के लिए किया जा सकता है।", + useCase: { + presets: { + name: "प्रीसेट", + description: + "आप विभिन्न टेक्स्ट/छवि सेट बना सकते हैं। एक पूर्व-परिभाषित है जिसे `generic` कहा जाता है। ध्यान दें, कोई डिफ़ॉल्ट नहीं है यदि आप पूरी तरह से कस्टम EmptyState चाहते हैं।", + }, + passingContent: { + name: "सामग्री पास करना", + description: "सामग्री पास करने के कई तरीके हैं।", + customizeImageHeading: "छवि को अनुकूलित करें", + customizeImageContent: "आप कोई भी छवि स्रोत पास कर सकते हैं।", + viaHeadingProp: "`heading` प्रॉप के माध्यम से", + viaContentProp: "`content` प्रॉप के माध्यम से।", + viaButtonProp: "`button` प्रॉप के माध्यम से", + }, + styling: { + name: "स्टाइलिंग", + description: "कंपोनेंट को आसानी से स्टाइल किया जा सकता है।", + }, + }, + }, +} + +export default demoHi diff --git a/app/i18n/demo-ja.ts b/app/i18n/demo-ja.ts new file mode 100644 index 00000000..c0e35589 --- /dev/null +++ b/app/i18n/demo-ja.ts @@ -0,0 +1,462 @@ +import { DemoTranslations } from "./demo-en" + +export const demoJa: DemoTranslations = { + demoIcon: { + description: + "あらかじめ登録されたアイコンを描画するコンポーネントです。 `onPress` が提供されている場合は にラップされますが、それ以外の場合は にラップされます。", + useCase: { + icons: { + name: "アイコン", + description: "登録されたアイコンのリストです。", + }, + size: { + name: "サイズ", + description: "sizeのpropsです。", + }, + color: { + name: "カラー", + description: "colorのpropsです。", + }, + styling: { + name: "スタイリング", + description: "このコンポーネントはスタイリングの変更ができます。", + }, + }, + }, + demoTextField: { + description: "このコンポーネントはテキストの入力と編集ができます。", + useCase: { + statuses: { + name: "ステータス", + description: + "status - これは他コンポーネントの`preset`の似ていますが、これはコンポーネントの機能も変えるpropsです。", + noStatus: { + label: "ステータスなし", + helper: "デフォルトのステータスです", + placeholder: "テキストが入力されます", + }, + error: { + label: "エラーステータス", + helper: "エラーが発生した場合に使用されるステータスです", + placeholder: "ここにテキストが入力されます", + }, + disabled: { + label: "無効(disabled)ステータス", + helper: "編集不可となるステータスです", + placeholder: "ここにテキストが入力されます", + }, + }, + passingContent: { + name: "コンテントを渡す", + description: "コンテントを渡す方法はいくつかあります。", + viaLabel: { + labelTx: "`label` から", + helper: "`helper` から", + placeholder: "`placeholder` から", + }, + rightAccessory: { + label: "右側にアクセサリー", + helper: "このpropsはReact要素を返す関数をうけとります。", + }, + leftAccessory: { + label: "左側にアクセサリー", + helper: "このpropsはReact要素を返す関数をうけとります。", + }, + supportsMultiline: { + label: "複数行サポート", + helper: "複数行の入力が出来るようになります。", + }, + }, + styling: { + name: "スタイリング", + description: "このコンポーネントはスタイリングの変更ができます。", + styleInput: { + label: "インプットのスタイル", + helper: "`style`から", + }, + styleInputWrapper: { + label: "インプットラッパーのスタイル", + helper: "`inputWrapperStyle`から", + }, + styleContainer: { + label: "スタイルコンテナのスタイル", + helper: "`containerStyle`から", + }, + styleLabel: { + label: "ラベルとヘルパーのスタイル", + helper: "`LabelTextProps` & `HelperTextProps`から", + }, + styleAccessories: { + label: "アクセサリーのスタイル", + helper: "`RightAccessory` & `LeftAccessory`から", + }, + }, + }, + }, + demoToggle: { + description: + "ブーリアンの入力を表示するコンポーネントです。コンポーネントはvalueの値を使用して描画するので、onValueChangeコールバックを使って値を変更し、valueを更新する必要があります。valueの値が変更されていない場合は、描画が更新されません。", + useCase: { + variants: { + name: "バリエーション", + description: + "このコンポーネントは数種類のバリエーションをサポートしています。もしカスタマイズが必要な場合、これらのバリエーションをリファクタリングできます。デフォルトは`checkbox`です。", + checkbox: { + label: "`checkbox`バリエーション", + helper: "シンプルなon/offのインプットに使えます。", + }, + radio: { + label: "`radio`バリエーション", + helper: "数個のオプションがある場合に使えます。", + }, + switch: { + label: "`switch`バリエーション", + helper: + "代表的なon/offのインプットです。他と比べアクセシビリティのサポートが充実しています。", + }, + }, + statuses: { + name: "ステータス", + description: + "status - これは他コンポーネントの`preset`の似ていますが、これはコンポーネントの機能も変えるpropsです。", + noStatus: "ステータスなし - デフォルトです。", + errorStatus: "エラー - エラーがある際に使えるステータスです。", + disabledStatus: "無効(disabled) - 編集不可となるステータスです", + }, + passingContent: { + name: "コンテントを渡す", + description: "コンテントを渡す方法はいくつかあります。", + useCase: { + checkBox: { + label: "`labelTx`から", + helper: "`helperTx`から", + }, + checkBoxMultiLine: { + helper: "複数行サポート - Nulla proident consectetur labore sunt ea labore. ", + }, + radioChangeSides: { + helper: "左右に変更 - Laborum labore adipisicing in eu ipsum deserunt.", + }, + customCheckBox: { + label: "カスタムアイコンも渡せます", + }, + switch: { + label: "スイッチはテキストとして読むこともできます。", + helper: + "デフォルトでは、このオプションはフォントの影響を受け、見た目が見苦しくなる可能性がある為`Text`コンポーネントを使用していません。必要に応じてカスタマイズしてください。", + }, + switchAid: { + label: "または補助アイコンもつけられます", + }, + }, + }, + styling: { + name: "スタイリング", + description: "このコンポーネントはスタイリングの変更ができます。", + outerWrapper: "1 - インプットの外側のラッパー", + innerWrapper: "2 - インプットの内側のラッパー", + inputDetail: "3 - インプットのそのもの", + labelTx: "ラベルのスタイルも変更できます。", + styleContainer: "もしくは、コンポーネントのコンテナ全体をスタイルすることもできます。", + }, + }, + }, + demoButton: { + description: + "ユーザーにアクションや選択を促すコンポーネントです。`Text`コンポーネントを`Pressable`コンポーネントでラップしています。", + useCase: { + presets: { + name: "プリセット", + description: "数種類のプリセットが用意されています。", + }, + passingContent: { + name: "コンテントを渡す", + description: "コンテントを渡す方法はいくつかあります。", + viaTextProps: "`text`から - Billum In", + children: "Childrenから - Irure Reprehenderit", + rightAccessory: "RightAccessoryから - Duis Quis", + leftAccessory: "LeftAccessoryから - Duis Proident", + nestedChildren: "ネストされたchildrenから - proident veniam.", + nestedChildren2: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + nestedChildren3: "Occaecat aliqua irure proident veniam.", + multiLine: + "Multilineから - consequat veniam veniam reprehenderit. Fugiat id nisi quis duis sunt proident mollit dolor mollit adipisicing proident deserunt.", + }, + styling: { + name: "スタイリング", + description: "このコンポーネントはスタイリングの変更ができます。", + styleContainer: "コンテナのスタイル - Exercitation", + styleText: "テキストのスタイル - Ea Anim", + styleAccessories: "アクセサリーのスタイル - enim ea id fugiat anim ad.", + pressedState: "押された状態のスタイル - fugiat anim", + }, + disabling: { + name: "無効化", + description: + "このコンポーネントは無効化できます。スタイルも同時に変更され、押した際の挙動も無効化されます。", + standard: "無効化 - standard", + filled: "無効化 - filled", + reversed: "無効化 - reversed", + accessory: "無効化されたアクセサリーのスタイル", + textStyle: "無効化されたテキストのスタイル", + }, + }, + }, + demoListItem: { + description: + "スタイルを指定されたリストの行のコンポーネントです。FlatListやSectionListなどのコンポーネントを使用することもできますし、単体でも使用できます。", + useCase: { + height: { + name: "高さ", + description: "高さの指定ができます。", + defaultHeight: "デフォルトの高さ (56px)", + customHeight: "`height`を使ったカスタムの高さ", + textHeight: + "テキストによって決まった高さ - Reprehenderit incididunt deserunt do do ea labore.", + longText: "テキストを1行に制限する- Reprehenderit incididunt deserunt do do ea labore.", + }, + separators: { + name: "セパレーター", + description: "セパレーター/ディバイダーは用意されてるかつ任意です。", + topSeparator: "トップセパレーターのみ", + topAndBottomSeparator: "トップとボトムのセパレーター", + bottomSeparator: "ボトムのセパレーター", + }, + icons: { + name: "アイコン", + description: "右または左のアイコンをカスタマイズすることができます。", + leftIcon: "左のアイコン", + rightIcon: "右のアイコン", + leftRightIcons: "左右のアイコン", + }, + customLeftRight: { + name: "左右のコンポーネントのカスタマイズ", + description: "左右のコンポーネントをカスタマイズすることができます。", + customLeft: "カスタムされた左コンポーネント", + customRight: "カスタムされた右コンポーネント", + }, + passingContent: { + name: "コンテントを渡す", + description: "コンテントを渡す方法はいくつかあります。", + text: "`text`から - reprehenderit sint", + children: "Childrenから - mostrud mollit", + nestedChildren1: "ネストされたchildrenから - proident veniam.", + nestedChildren2: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + }, + listIntegration: { + name: "FlatList & FlashListに組みこむ場合", + description: + "このコンポーネントはお好みのリスト系のコンポーネントへ容易に組み込むことができます。", + }, + styling: { + name: "スタイリング", + description: "このコンポーネントはスタイリングの変更ができます。", + styledText: "スタイルされたテキスト", + styledContainer: "スタイルされたコンテナ(セパレーター)", + tintedIcons: "アイコンに色をつける", + }, + }, + }, + demoCard: { + description: + "カードは関連する情報同士をまとめるのに役立ちます。ListItemが横に情報を表示するのに使え、こちらは縦に表示するのに使えます。", + useCase: { + presets: { + name: "プリセット", + description: "数種類のプリセットが用意されています。", + default: { + heading: "デフォルトのプリセット", + content: "Incididunt magna ut aliquip consectetur mollit dolor.", + footer: "Consectetur nulla non aliquip velit.", + }, + reversed: { + heading: "リバースのプリセット", + content: "Reprehenderit occaecat proident amet id laboris.", + footer: "Consectetur tempor ea non labore anim .", + }, + }, + verticalAlignment: { + name: "縦の位置調整", + description: "カードは用意されたプリセットを使っての縦位置調整ができます。", + top: { + heading: "Top(デフォルト)", + content: "全てのコンテンツは自動的に上に配置されます。", + footer: "Footerも同じように上に配置されます。", + }, + center: { + heading: "センター", + content: "全てのコンテンツはカードの高さから見て中央に配置されます。", + footer: "Footerである私も!", + }, + spaceBetween: { + heading: "Space Between", + content: "全てのコンテンツは均等に分配されます。", + footer: "Footerの私はここが一番落ち着くね", + }, + reversed: { + heading: "Footerのみを下に配置する", + content: "その名の通り、Footerのみを下に配置することができます。", + footer: "Footerは一人で寂しい", + }, + }, + passingContent: { + name: "コンテントを渡す", + description: "コンテントを渡す方法はいくつかあります。", + heading: "`heading`から", + content: "`content`から", + footer: "`footer`から", + }, + customComponent: { + name: "カスタムコンポーネント", + description: + "全てのプリセットはカスタムコンポーネントを使って拡張/変更することができます。", + rightComponent: "右コンポーネント", + leftComponent: "左コンポーネント", + }, + style: { + name: "スタイリング", + description: "このコンポーネントはスタイリングの変更ができます。", + heading: "ヘディングのスタイル", + content: "コンテントのスタイル", + footer: "フッターのスタイル", + }, + }, + }, + demoAutoImage: { + description: "リモートまたはデータURIによって自動的にサイズを変更する画像コンポーネントです。", + useCase: { + remoteUri: { name: "リモート URI" }, + base64Uri: { name: "Base64 URI" }, + scaledToFitDimensions: { + name: "ディメンションにフィットするように拡大する", + description: + "`maxWidth` と/または `maxHeight`を指定することで、アスペクト比を維持したままサイズを変更することができます。`resizeMode: 'contain'`との違いとしては: \n1. 一方のサイズの指定でも良い(両方の指定の必要がない)。 \n2. 画像のコンテナに押し込められるのではなく、画像のディメンションを保ったまま指定したサイズに拡大、縮小を行うことができます。", + heightAuto: "width: 60 / height: auto", + widthAuto: "width: auto / height: 32", + bothManual: "width: 60 / height: 60", + }, + }, + }, + demoText: { + description: + "テキストを表示する為のコンポーネントです。これはReact NativeのTextコンポーネントを内包する高階コンポーネント(Higher Order Component)です。", + useCase: { + presets: { + name: "プリセット", + description: "数種類のプリセットが用意されています。", + default: + "デフォルトのプリセット - Cillum eu laboris in labore. Excepteur mollit tempor reprehenderit fugiat elit et eu consequat laborum.", + bold: "ボールドのプリセット - Tempor et ullamco cupidatat in officia. Nulla ea duis elit id sunt ipsum cillum duis deserunt nostrud ut nostrud id.", + subheading: "サブヘディングのプリセット - In Cupidatat Cillum.", + heading: "ヘディングのプリセット - Voluptate Adipis.", + }, + sizes: { + name: "サイズ", + description: "サイズ用のpropsです.", + xs: "xs - Ea ipsum est ea ex sunt.", + sm: "sm - Lorem sunt adipisicin.", + md: "md - Consequat id do lorem.", + lg: "lg - Nostrud ipsum ea.", + xl: "xl - Eiusmod ex excepteur.", + xxl: "xxl - Cillum eu laboris.", + }, + weights: { + name: "ウエイト", + description: "ウエイト用のpropです。", + light: + "ライト - Nulla magna incididunt excepteur est occaecat duis culpa dolore cupidatat enim et.", + normal: + "ノーマル - Magna incididunt dolor ut veniam veniam laboris aliqua velit ea incididunt.", + medium: "ミディアム - Non duis laborum quis laboris occaecat culpa cillum.", + semibold: "セミボールド - Exercitation magna nostrud pariatur laborum occaecat aliqua.", + bold: "ボールド - Eiusmod ullamco magna exercitation est excepteur.", + }, + passingContent: { + name: "コンテントを渡す", + description: "コンテントを渡す方法はいくつかあります。", + viaText: + "`text`から - Billum in aute fugiat proident nisi pariatur est. Cupidatat anim cillum eiusmod ad. Officia eu magna aliquip labore dolore consequat.", + viaTx: "`tx`から -", + children: "childrenから - Aliqua velit irure reprehenderit eu qui amet veniam consectetur.", + nestedChildren: "ネストされたchildrenから -", + nestedChildren2: "Occaecat aliqua irure proident veniam.", + nestedChildren3: "Ullamco cupidatat officia exercitation velit non ullamco nisi..", + nestedChildren4: "Occaecat aliqua irure proident veniam.", + }, + styling: { + name: "スタイリング", + description: "このコンポーネントはスタイリングの変更ができます。", + text: "Consequat ullamco veniam velit mollit proident excepteur aliquip id culpa ipsum velit sint nostrud.", + text2: + "Eiusmod occaecat laboris eu ex veniam ipsum adipisicing consectetur. Magna ullamco adipisicing tempor adipisicing.", + text3: + "Eiusmod occaecat laboris eu ex veniam ipsum adipisicing consectetur. Magna ullamco adipisicing tempor adipisicing.", + }, + }, + }, + demoHeader: { + description: + "様々なスクリーンで登場するコンポーネントです。ナビゲーションのボタンとスクリーンタイトルを含みます。", + useCase: { + actionIcons: { + name: "アクションアイコン", + description: "左右にアイコンを表示させることができます。", + leftIconTitle: "左アイコン", + rightIconTitle: "右アイコン", + bothIconsTitle: "両方のアイコン", + }, + actionText: { + name: "アクションテキスト", + description: "左右にテキストを表示させることができます。", + leftTxTitle: "`leftTx`から", + rightTextTitle: "`rightText`から", + }, + customActionComponents: { + name: "カスタムアクションコンポーネント", + description: + "アイコンまたはテキスト以外のものが必要な場合は、カスタムのアクションコンポーネントを渡すことができます。", + customLeftActionTitle: "カスタムの左アクション", + }, + titleModes: { + name: "タイトルモード", + description: + "タイトルはデフォルトで中央に配置されますが、長すぎるとカットされてしまいます。Flexを使うことでアクションボタンから自動的にポジションを調整することもできます。", + centeredTitle: "Centered Title", + flexTitle: "Flex Title", + }, + styling: { + name: "スタイリング", + description: "このコンポーネントはスタイリングの変更ができます。", + styledTitle: "スタイルされたタイトル", + styledWrapperTitle: "スタイルされたラッパー", + tintedIconsTitle: "色付けされたアイコン", + }, + }, + }, + demoEmptyState: { + description: + "表示する為のデータが存在しない場合に使えるコンポーネントです。ユーザーに取るべきアクションをお勧めする際に有用です。", + useCase: { + presets: { + name: "プリセット", + description: + "text/imageのセットを使ってカスタマイズすることができます。これは`generic`のものです。カスタマイズが必要になることを想定して、このコンポーネントにデフォルトのプリセットは存在しません。", + }, + passingContent: { + name: "コンテントを渡す", + description: "コンテントを渡す方法はいくつかあります。", + customizeImageHeading: "画像をカスタマイズ", + customizeImageContent: "画像のソースを渡すことができます。", + viaHeadingProp: "`heading`から", + viaContentProp: "`content`から", + viaButtonProp: "`button`から", + }, + styling: { + name: "スタイリング", + description: "このコンポーネントはスタイリングの変更ができます。", + }, + }, + }, +} + +export default demoJa diff --git a/app/i18n/demo-ko.ts b/app/i18n/demo-ko.ts new file mode 100644 index 00000000..61ef883d --- /dev/null +++ b/app/i18n/demo-ko.ts @@ -0,0 +1,455 @@ +import { DemoTranslations } from "./demo-en" + +export const demoKo: DemoTranslations = { + demoIcon: { + description: + "등록된 아이콘을 렌더링하는 컴포넌트입니다. `onPress`가 구현되어 있으면 로, 그렇지 않으면 로 감쌉니다.", + useCase: { + icons: { + name: "아이콘", + description: "컴포넌트에 등록된 아이콘 목록입니다.", + }, + size: { + name: "크기", + description: "크기 속성이 있습니다.", + }, + color: { + name: "색상", + description: "색상 속성이 있습니다.", + }, + styling: { + name: "스타일링", + description: "컴포넌트는 쉽게 스타일링할 수 있습니다.", + }, + }, + }, + demoTextField: { + description: "TextField 컴포넌트는 텍스트 입력 및 편집을 허용합니다.", + useCase: { + statuses: { + name: "상태", + description: + "다른 컴포넌트의 `preset`과 유사한 상태 속성이 있으며, 컴포넌트의 기능에도 영향을 미칩니다.", + noStatus: { + label: "상태 없음", + helper: "이것이 기본 상태입니다", + placeholder: "텍스트가 여기에 들어갑니다", + }, + error: { + label: "오류 상태", + helper: "오류가 있을 때 사용하는 상태입니다", + placeholder: "텍스트가 여기에 들어갑니다", + }, + disabled: { + label: "비활성 상태", + helper: "편집 기능을 비활성화하고 텍스트를 표시하지 않습니다", + placeholder: "텍스트가 여기에 들어갑니다", + }, + }, + passingContent: { + name: "내용 전달", + description: "내용을 전달하는 몇 가지 방법이 있습니다.", + viaLabel: { + labelTx: "`label` 속성으로", + helper: "`helper` 속성으로", + placeholder: "`placeholder` 속성으로", + }, + rightAccessory: { + label: "오른쪽 액세서리", + helper: "이 속성은 React 요소를 반환하는 함수를 받습니다.", + }, + leftAccessory: { + label: "왼쪽 액세서리", + helper: "이 속성은 React 요소를 반환하는 함수를 받습니다.", + }, + supportsMultiline: { + label: "멀티라인 지원", + helper: "멀티라인 텍스트를 위한 더 높은 입력을 활성화합니다.", + }, + }, + styling: { + name: "스타일링", + description: "컴포넌트는 쉽게 스타일링할 수 있습니다.", + styleInput: { + label: "입력 스타일", + helper: "`style` 속성으로", + }, + styleInputWrapper: { + label: "입력 래퍼 스타일", + helper: "`inputWrapperStyle` 속성으로", + }, + styleContainer: { + label: "컨테이너 스타일", + helper: "`containerStyle` 속성으로", + }, + styleLabel: { + label: "레이블 및 헬퍼 스타일", + helper: "`LabelTextProps` 및 `HelperTextProps` 스타일 속성으로", + }, + styleAccessories: { + label: "액세서리 스타일", + helper: "`RightAccessory` 및 `LeftAccessory` 스타일 속성으로", + }, + }, + }, + }, + demoToggle: { + description: + "불리언 입력을 렌더링합니다. 사용자가 수행한 작업을 반영하기 위해 값 속성을 업데이트하는 onValueChange 콜백이 필요한 제어된 컴포넌트입니다. 값 속성이 업데이트되지 않으면, 컴포넌트는 사용자 작업의 예상 결과 대신 제공된 값 속성을 계속 렌더링합니다.", + useCase: { + variants: { + name: "변형", + description: + "이 컴포넌트는 몇 가지 변형을 지원합니다. 특정 변형을 대폭 커스터마이즈해야 하는 경우에는 쉽게 리팩토링할 수 있습니다. 기본값은 `체크박스`입니다.", + checkbox: { + label: "`체크박스` 변형", + helper: "단일 켜기/끄기 입력에 사용할 수 있습니다.", + }, + radio: { + label: "`라디오` 변형", + helper: "여러 옵션이 있는 경우 사용하십시오.", + }, + switch: { + label: "`스위치` 변형", + helper: "더 눈에 띄는 켜기/끄기 입력입니다. 접근성 지원이 더 좋습니다.", + }, + }, + statuses: { + name: "상태", + description: + "다른 컴포넌트의 `preset`과 유사한 상태 속성이 있으며, 컴포넌트의 기능에도 영향을 미칩니다.", + noStatus: "상태 없음 - 기본 상태", + errorStatus: "오류 상태 - 오류가 있을 때 사용", + disabledStatus: "비활성 상태 - 편집 기능을 비활성화하고 입력을 표시하지 않음", + }, + passingContent: { + name: "내용 전달", + description: "내용을 전달하는 몇 가지 방법이 있습니다.", + useCase: { + checkBox: { + label: "`labelTx` 속성으로", + helper: "`helperTx` 속성으로", + }, + checkBoxMultiLine: { + helper: "멀티라인 지원 - 멀티라인 지원을 위한 예제 문장입니다. 하나 둘 셋.", + }, + radioChangeSides: { + helper: "양쪽을 변경할 수 있습니다 - 양쪽 변경을 위한 예제 문장입니다. 하나 둘 셋.", + }, + customCheckBox: { + label: "맞춤 체크박스 아이콘 전달.", + }, + switch: { + label: "스위치는 텍스트로 읽을 수 있습니다", + helper: + "기본적으로 이 옵션은 `Text`를 사용하지 않습니다. 폰트에 따라 켜기/끄기 문자가 이상하게 보일 수 있기 때문입니다. 필요에 따라 커스터마이즈하세요.", + }, + switchAid: { + label: "또는 아이콘으로 보조", + }, + }, + }, + styling: { + name: "스타일링", + description: "컴포넌트는 쉽게 스타일링할 수 있습니다.", + outerWrapper: "1 - 입력 외부 래퍼 스타일링", + innerWrapper: "2 - 입력 내부 래퍼 스타일링", + inputDetail: "3 - 입력 디테일 스타일링", + labelTx: "labelTx도 스타일링할 수 있습니다", + styleContainer: "또는 전체 컨테이너 스타일링", + }, + }, + }, + demoButton: { + description: + "사용자가 작업을 수행하고 선택을 할 수 있도록 하는 컴포넌트입니다. Text 컴포넌트를 Pressable 컴포넌트로 감쌉니다.", + useCase: { + presets: { + name: "프리셋", + description: "사전 구성된 몇 가지 프리셋이 있습니다.", + }, + passingContent: { + name: "내용 전달", + description: "내용을 전달하는 몇 가지 방법이 있습니다.", + viaTextProps: "`text` 속성으로 - 예제 문장입니다.", + children: "자식 - 또 다른 예제 문장입니다.", + rightAccessory: "오른쪽 액세서리 - 예제 문장입니다.", + leftAccessory: "왼쪽 액세서리 - 예제 문장입니다.", + nestedChildren: "중첩 자식 - 별 하나에 추억과 별 하나에 사랑과 별 하나에 쓸쓸함과", + nestedChildren2: "별 하나에 동경과 별 하나에 시와 ", + nestedChildren3: "별 하나에 어머니, 어머니.", + multiLine: + "멀티라인 - 죽는 날까지 하늘을 우러러 한 점 부끄럼이 없기를, 잎새에 이는 바람에도 나는 괴로워했다. 별을 노래하는 마음으로 모든 죽어 가는 것을 사랑해야지 그리고 나한테 주어진 길을 걸어가야겠다. 오늘 밤에도 별이 바람에 스치운다.", + }, + styling: { + name: "스타일링", + description: "컴포넌트는 쉽게 스타일링할 수 있습니다.", + styleContainer: "스타일 컨테이너 - 예제 문장", + styleText: "스타일 텍스트 - 예제 문장", + styleAccessories: "스타일 액세서리 - 또 다른 예제 문장", + pressedState: "스타일 눌린 상태 - 예제 문장", + }, + disabling: { + name: "비활성화", + description: + "컴포넌트는 비활성화할 수 있으며, 그에 따라 스타일링할 수 있습니다. 누르는 동작이 비활성화됩니다.", + standard: "비활성화 - 표준", + filled: "비활성화 - 채워진", + reversed: "비활성화 - 역방향", + accessory: "비활성화된 액세서리 스타일", + textStyle: "비활성화된 텍스트 스타일", + }, + }, + }, + demoListItem: { + description: "FlatList, SectionList 또는 자체적으로 사용할 수 있는 스타일된 행 컴포넌트입니다.", + useCase: { + height: { + name: "높이", + description: "행은 다른 높이를 가질 수 있습니다.", + defaultHeight: "기본 높이 (56px)", + customHeight: "`height` 속성을 통해 사용자 정의 높이", + textHeight: + "텍스트 내용에 의해 결정된 높이 - 예제를 위한 긴 문장입니다. 하나 둘 셋. 안녕하세요.", + longText: + "긴 텍스트를 한 줄로 제한 - 이것 역시 예제를 위한 긴 문장입니다. 오늘 날씨는 어떤가요?", + }, + separators: { + name: "구분선", + description: "구분선 / 디바이더가 사전 구성되어 있으며 선택 사항입니다.", + topSeparator: "상단 구분선만", + topAndBottomSeparator: "상단 및 하단 구분선", + bottomSeparator: "하단 구분선만", + }, + icons: { + name: "아이콘", + description: "왼쪽 또는 오른쪽 아이콘을 사용자 정의할 수 있습니다.", + leftIcon: "왼쪽 아이콘", + rightIcon: "오른쪽 아이콘", + leftRightIcons: "왼쪽 및 오른쪽 아이콘", + }, + customLeftRight: { + name: "사용자 정의 왼쪽/오른쪽 컴포넌트", + description: "필요시에는 사용자가 정의한 왼쪽/오른쪽 컴포넌트를 전달할 수 있습니다.", + customLeft: "사용자 정의 왼쪽 컴포넌트", + customRight: "사용자 정의 오른쪽 컴포넌트", + }, + passingContent: { + name: "내용 전달", + description: "내용을 전달하는 몇 가지 방법이 있습니다.", + text: "`text` 속성으로 - 예제 문장입니다.", + children: "자식 - 또 다른 예제 문장입니다.", + nestedChildren1: "중첩 자식 - 이것도 예제 문장입니다..", + nestedChildren2: "또 다른 예제 문장, 중첩이 된 형태입니다.", + }, + listIntegration: { + name: "FlatList 및 FlashList 통합", + description: "이 컴포넌트는 선호하는 리스트 인터페이스와 쉽게 통합할 수 있습니다.", + }, + styling: { + name: "스타일링", + description: "컴포넌트는 쉽게 스타일링할 수 있습니다.", + styledText: "스타일된 텍스트", + styledContainer: "스타일된 컨테이너 (구분선)", + tintedIcons: "색이 입혀진 아이콘", + }, + }, + }, + demoCard: { + description: + "카드는 관련 정보를 컨테이너에 담아 표시하는 데 유용합니다. ListItem이 내용을 수평으로 표시한다면, 카드는 내용을 수직으로 표시할 수 있습니다.", + useCase: { + presets: { + name: "프리셋", + description: "사전 구성된 몇 가지 프리셋이 있습니다.", + default: { + heading: "기본 프리셋 (기본값)", + content: "예제 문장입니다. 그믐밤 반디불은 부서진 달조각", + footer: "숲으로 가자 달조각을 주으려 숲으로 가자.", + }, + reversed: { + heading: "역방향 프리셋", + content: "예제 문장입니다. 그믐밤 반디불은 부서진 달조각", + footer: "숲으로 가자 달조각을 주으려 숲으로 가자.", + }, + }, + verticalAlignment: { + name: "수직 정렬", + description: "카드는 필요에 따라 미리 구성된 다양한 정렬방법으로 제공됩니다.", + top: { + heading: "상단 (기본값)", + content: "모든 콘텐츠가 자동으로 상단에 정렬됩니다.", + footer: "심지어 푸터도", + }, + center: { + heading: "중앙", + content: "콘텐츠는 카드 높이에 상대적으로 중앙에 배치됩니다.", + footer: "나도!", + }, + spaceBetween: { + heading: "공간 사이", + content: "모든 콘텐츠가 고르게 간격을 둡니다.", + footer: "나는 내가 있고 싶은 곳에 있어요.", + }, + reversed: { + heading: "푸터 강제 하단", + content: "푸터를 원하는 위치에 밀어 넣습니다.", + footer: "여기 너무 외로워요.", + }, + }, + passingContent: { + name: "내용 전달", + description: "내용을 전달하는 몇 가지 방법이 있습니다.", + heading: "`heading` 속성으로", + content: "`content` 속성으로", + footer: "푸터도 외로워요.", + }, + customComponent: { + name: "사용자 정의 컴포넌트", + description: + "사전 구성된 컴포넌트 중 하나를 직접 만든 자신의 컴포넌트로 대체할 수 있습니다. 추가 컴포넌트도 덧붙여 넣을 수 있습니다.", + rightComponent: "오른쪽 컴포넌트", + leftComponent: "왼쪽 컴포넌트", + }, + style: { + name: "스타일링", + description: "컴포넌트는 쉽게 스타일링할 수 있습니다.", + heading: "헤딩 스타일링", + content: "컨텐츠 스타일링", + footer: "푸터 스타일링", + }, + }, + }, + demoAutoImage: { + description: "원격 또는 data-uri 이미지의 크기를 자동으로 조정하는 Image 컴포넌트입니다.", + useCase: { + remoteUri: { name: "원격 URI" }, + base64Uri: { name: "Base64 URI" }, + scaledToFitDimensions: { + name: "치수에 맞게 조정", + description: + "`maxWidth` 단독으로, 혹은 `maxHeight` 속성과 함께 제공하면, 이미지는 비율을 유지하면서 자동으로 크기가 조정됩니다. 이것이 `resizeMode: 'contain'`과 다른 점은 무엇일까요? 첫째, 한쪽 크기만 지정할 수 있습니다. 둘째, 이미지가 이미지 컨테이너 내에 포함되는 대신 원하는 치수에 맞게 조정됩니다.", + heightAuto: "너비: 60 / 높이: 자동", + widthAuto: "너비: 자동 / 높이: 32", + bothManual: "너비: 60 / 높이: 60", + }, + }, + }, + demoText: { + description: + "텍스트 표시가 필요한 경우를 위해, 이 컴포넌트는 기본 React Native 컴포넌트 위에 HOC로 제작되었습니다.", + useCase: { + presets: { + name: "프리셋", + description: "사전 구성된 몇 가지 프리셋이 있습니다.", + default: "기본 프리셋 - 예제 문장입니다. 하나 둘 셋.", + bold: "볼드 프리셋 - 예제 문장입니다. 하나 둘 셋.", + subheading: "서브헤딩 프리셋 - 예제 문장입니다. 하나 둘 셋.", + heading: "헤딩 프리셋 - 예제 문장입니다. 하나 둘 셋.", + }, + sizes: { + name: "크기", + description: "크기 속성이 있습니다.", + xs: "xs - 조금 더 작은 크기 속성입니다.", + sm: "sm - 작은 크기 속성입니다.", + md: "md - 중간 크기 속성입니다.", + lg: "lg - 큰 크기 속성입니다.", + xl: "xl - 조금 더 큰 크기 속성입니다.", + xxl: "xxl - 아주 큰 크기 속성입니다.", + }, + weights: { + name: "굵기", + description: "굵기 속성이 있습니다.", + light: "가벼움 - 예제 문장입니다. 안녕하세요. 하나 둘 셋.", + normal: "보통 - 예제 문장입니다. 안녕하세요. 하나 둘 셋.", + medium: "중간 - 예제 문장입니다. 안녕하세요. 하나 둘 셋.", + semibold: "세미볼드 - 예제 문장입니다. 안녕하세요. 하나 둘 셋.", + bold: "볼드 - 예제 문장입니다. 안녕하세요. 하나 둘 셋.", + }, + passingContent: { + name: "내용 전달", + description: "내용을 전달하는 몇 가지 방법이 있습니다.", + viaText: + "`text` 속성으로 - 죽는 날까지 하늘을 우러러 한 점 부끄럼이 없기를, 잎새에 이는 바람에도 나는 괴로워했다. 별을 노래하는 마음으로 모든 죽어 가는 것을 사랑해야지 그리고 나한테 주어진 길을 걸어가야겠다. 오늘 밤에도 별이 바람에 스치운다.", + viaTx: "`tx` 속성으로", + children: "자식 - 또 다른 예제 문장입니다. 하나 둘 셋.", + nestedChildren: "중첩 자식", + nestedChildren2: "죽는 날까지 하늘을 우러러 한 점 부끄럼이 없기를, ", + nestedChildren3: "잎새에 이는 바람에도 나는 괴로워했다.", + nestedChildren4: "별을 노래하는 마음으로 모든 죽어 가는 것을 사랑해야지.", + }, + styling: { + name: "스타일링", + description: "컴포넌트는 쉽게 스타일링할 수 있습니다.", + text: "그리고 나한테 주어진 길을 걸어가야겠다.", + text2: "오늘 밤에도 별이 바람에 스치운다.", + text3: "계속 이어지는 예제 문장입니다. 하나 둘 셋.", + }, + }, + }, + demoHeader: { + description: + "여러 화면에 나타나는 컴포넌트입니다. 네비게이션 버튼과 화면 제목을 포함할 것입니다.", + useCase: { + actionIcons: { + name: "액션 아이콘", + description: "왼쪽 또는 오른쪽 액션 컴포넌트에 아이콘을 쉽게 전달할 수 있습니다.", + leftIconTitle: "왼쪽 아이콘", + rightIconTitle: "오른쪽 아이콘", + bothIconsTitle: "양쪽 아이콘", + }, + actionText: { + name: "액션 텍스트", + description: "왼쪽 또는 오른쪽 액션 컴포넌트에 텍스트를 쉽게 전달할 수 있습니다.", + leftTxTitle: "`leftTx`를 통해", + rightTextTitle: "`rightText`를 통해", + }, + customActionComponents: { + name: "사용자 정의 액션 컴포넌트", + description: + "아이콘이나 텍스트 옵션이 충분하지 않은 경우, 사용자 정의 액션 컴포넌트를 전달할 수 있습니다.", + customLeftActionTitle: "사용자 정의 왼쪽 액션", + }, + titleModes: { + name: "제목 모드", + description: + "제목은 기본적으로 중앙에 고정되지만 너무 길면 잘릴 수 있습니다. 액션 버튼에 맞춰 조정할 수 있습니다.", + centeredTitle: "중앙 제목", + flexTitle: "유연한 제목", + }, + styling: { + name: "스타일링", + description: "컴포넌트는 쉽게 스타일링할 수 있습니다.", + styledTitle: "스타일된 제목", + styledWrapperTitle: "스타일된 래퍼", + tintedIconsTitle: "색이 입혀진 아이콘", + }, + }, + }, + demoEmptyState: { + description: + "표시할 데이터가 없을 때 사용할 수 있는 컴포넌트입니다. 사용자가 다음에 무엇을 할지 안내할 수 있습니다.", + useCase: { + presets: { + name: "프리셋", + description: + "다양한 텍스트/이미지 세트를 만들 수 있습니다. `generic`이라는 사전 정의된 세트가 하나 있습니다. 기본값이 없으므로 완전히 사용자 정의된 EmptyState를 원할 경우 사용할 수 있습니다.", + }, + passingContent: { + name: "내용 전달", + description: "내용을 전달하는 몇 가지 방법이 있습니다.", + customizeImageHeading: "이미지 맞춤 설정", + customizeImageContent: "어떤 이미지 소스도 전달할 수 있습니다.", + viaHeadingProp: "`heading` 속성으로", + viaContentProp: "`content` 속성으로", + viaButtonProp: "`button` 속성으로", + }, + styling: { + name: "스타일링", + description: "컴포넌트는 쉽게 스타일링할 수 있습니다.", + }, + }, + }, +} + +export default demoKo diff --git a/app/i18n/en.ts b/app/i18n/en.ts new file mode 100644 index 00000000..7ab6f8ae --- /dev/null +++ b/app/i18n/en.ts @@ -0,0 +1,129 @@ +import demoEn from "./demo-en" + +const en = { + common: { + ok: "OK!", + cancel: "Cancel", + back: "Back", + logOut: "Log Out", + }, + welcomeScreen: { + postscript: + "psst — This probably isn't what your app looks like. (Unless your designer handed you these screens, and in that case, ship it!)", + readyForLaunch: "Your app, almost ready for launch!", + exciting: "(ohh, this is exciting!)", + letsGo: "Let's go!", + }, + errorScreen: { + title: "Something went wrong!", + friendlySubtitle: + "This is the screen that your users will see in production when an error is thrown. You'll want to customize this message (located in `app/i18n/en.ts`) and probably the layout as well (`app/screens/ErrorScreen`). If you want to remove this entirely, check `app/app.tsx` for the component.", + reset: "RESET APP", + traceTitle: "Error from %{name} stack", + }, + emptyStateComponent: { + generic: { + heading: "So empty... so sad", + content: "No data found yet. Try clicking the button to refresh or reload the app.", + button: "Let's try this again", + }, + }, + + errors: { + invalidEmail: "Invalid email address.", + }, + loginScreen: { + logIn: "Log In", + enterDetails: + "Enter your details below to unlock top secret info. You'll never guess what we've got waiting. Or maybe you will; it's not rocket science here.", + emailFieldLabel: "Email", + passwordFieldLabel: "Password", + emailFieldPlaceholder: "Enter your email address", + passwordFieldPlaceholder: "Super secret password here", + tapToLogIn: "Tap to log in!", + hint: "Hint: you can use any email address and your favorite password :)", + }, + demoNavigator: { + componentsTab: "Components", + debugTab: "Debug", + communityTab: "Community", + podcastListTab: "Podcast", + }, + demoCommunityScreen: { + title: "Connect with the community", + tagLine: + "Plug in to Infinite Red's community of React Native engineers and level up your app development with us!", + joinUsOnSlackTitle: "Join us on Slack", + joinUsOnSlack: + "Wish there was a place to connect with React Native engineers around the world? Join the conversation in the Infinite Red Community Slack! Our growing community is a safe space to ask questions, learn from others, and grow your network.", + joinSlackLink: "Join the Slack Community", + makeIgniteEvenBetterTitle: "Make Ignite even better", + makeIgniteEvenBetter: + "Have an idea to make Ignite even better? We're happy to hear that! We're always looking for others who want to help us build the best React Native tooling out there. Join us over on GitHub to join us in building the future of Ignite.", + contributeToIgniteLink: "Contribute to Ignite", + theLatestInReactNativeTitle: "The latest in React Native", + theLatestInReactNative: "We're here to keep you current on all React Native has to offer.", + reactNativeRadioLink: "React Native Radio", + reactNativeNewsletterLink: "React Native Newsletter", + reactNativeLiveLink: "React Native Live", + chainReactConferenceLink: "Chain React Conference", + hireUsTitle: "Hire Infinite Red for your next project", + hireUs: + "Whether it's running a full project or getting teams up to speed with our hands-on training, Infinite Red can help with just about any React Native project.", + hireUsLink: "Send us a message", + }, + demoShowroomScreen: { + jumpStart: "Components to jump start your project!", + lorem2Sentences: + "Nulla cupidatat deserunt amet quis aliquip nostrud do adipisicing. Adipisicing excepteur elit laborum Lorem adipisicing do duis.", + demoHeaderTxExample: "Yay", + demoViaTxProp: "Via `tx` Prop", + demoViaSpecifiedTxProp: "Via `{{prop}}Tx` Prop", + }, + demoDebugScreen: { + howTo: "HOW TO", + title: "Debug", + tagLine: + "Congratulations, you've got a very advanced React Native app template here. Take advantage of this boilerplate!", + reactotron: "Send to Reactotron", + reportBugs: "Report Bugs", + demoList: "Demo List", + demoPodcastList: "Demo Podcast List", + androidReactotronHint: + "If this doesn't work, ensure the Reactotron desktop app is running, run adb reverse tcp:9090 tcp:9090 from your terminal, and reload the app.", + iosReactotronHint: + "If this doesn't work, ensure the Reactotron desktop app is running and reload app.", + macosReactotronHint: + "If this doesn't work, ensure the Reactotron desktop app is running and reload app.", + webReactotronHint: + "If this doesn't work, ensure the Reactotron desktop app is running and reload app.", + windowsReactotronHint: + "If this doesn't work, ensure the Reactotron desktop app is running and reload app.", + }, + demoPodcastListScreen: { + title: "React Native Radio episodes", + onlyFavorites: "Only Show Favorites", + favoriteButton: "Favorite", + unfavoriteButton: "Unfavorite", + accessibility: { + cardHint: + "Double tap to listen to the episode. Double tap and hold to {{action}} this episode.", + switch: "Switch on to only show favorites", + favoriteAction: "Toggle Favorite", + favoriteIcon: "Episode not favorited", + unfavoriteIcon: "Episode favorited", + publishLabel: "Published {{date}}", + durationLabel: "Duration: {{hours}} hours {{minutes}} minutes {{seconds}} seconds", + }, + noFavoritesEmptyState: { + heading: "This looks a bit empty", + content: + "No favorites have been added yet. Tap the heart on an episode to add it to your favorites!", + }, + }, + + ...demoEn, +} + +export default en +export type Translations = typeof en diff --git a/app/i18n/es.ts b/app/i18n/es.ts new file mode 100644 index 00000000..7f30d2ac --- /dev/null +++ b/app/i18n/es.ts @@ -0,0 +1,131 @@ +import demoEs from "./demo-es" +import { Translations } from "./en" + +const es: Translations = { + common: { + ok: "OK", + cancel: "Cancelar", + back: "Volver", + logOut: "Cerrar sesión", + }, + welcomeScreen: { + postscript: + "psst — Esto probablemente no es cómo se va a ver tu app. (A menos que tu diseñador te haya enviado estas pantallas, y en ese caso, ¡lánzalas en producción!)", + readyForLaunch: "Tu app, casi lista para su lanzamiento", + exciting: "(¡ohh, esto es emocionante!)", + letsGo: "¡Vamos!", + }, + errorScreen: { + title: "¡Algo salió mal!", + friendlySubtitle: + "Esta es la pantalla que verán tus usuarios en producción cuando haya un error. Vas a querer personalizar este mensaje (que está ubicado en `app/i18n/es.ts`) y probablemente también su diseño (`app/screens/ErrorScreen`). Si quieres eliminarlo completamente, revisa `app/app.tsx` y el componente .", + reset: "REINICIA LA APP", + traceTitle: "Error desde %{name}", + }, + emptyStateComponent: { + generic: { + heading: "Muy vacío... muy triste", + content: + "No se han encontrado datos por el momento. Intenta darle clic en el botón para refrescar o recargar la app.", + button: "Intentemos de nuevo", + }, + }, + + errors: { + invalidEmail: "Email inválido.", + }, + loginScreen: { + logIn: "Iniciar sesión", + enterDetails: + "Ingresa tus datos a continuación para desbloquear información ultra secreta. Nunca vas a adivinar lo que te espera al otro lado. O quizás si lo harás; la verdad no hay mucha ciencia alrededor.", + emailFieldLabel: "Email", + passwordFieldLabel: "Contraseña", + emailFieldPlaceholder: "Ingresa tu email", + passwordFieldPlaceholder: "Contraseña super secreta aquí", + tapToLogIn: "¡Presiona acá para iniciar sesión!", + hint: "Consejo: puedes usar cualquier email y tu contraseña preferida :)", + }, + demoNavigator: { + componentsTab: "Componentes", + debugTab: "Debug", + communityTab: "Comunidad", + podcastListTab: "Podcasts", + }, + demoCommunityScreen: { + title: "Conecta con la comunidad", + tagLine: + "Únete a la comunidad React Native con los ingenieros de Infinite Red y mejora con nosotros tus habilidades para el desarrollo de apps.", + joinUsOnSlackTitle: "Únete a nosotros en Slack", + joinUsOnSlack: + "¿Quieres conectar con desarrolladores de React Native de todo el mundo? Únete a la conversación en nuestra comunidad de Slack. Nuestra comunidad, que crece día a día, es un espacio seguro para hacer preguntas, aprender de los demás y ampliar tu red.", + joinSlackLink: "Únete a la comunidad de Slack", + makeIgniteEvenBetterTitle: "Haz que Ignite sea aún mejor", + makeIgniteEvenBetter: + "¿Tienes una idea para hacer que Ignite sea aún mejor? ¡Nos encantaría escucharla! Estamos siempre buscando personas que quieran ayudarnos a construir las mejores herramientas para React Native. Únete a nosotros en GitHub para ayudarnos a construir el futuro de Ignite.", + contributeToIgniteLink: "Contribuir a Ignite", + theLatestInReactNativeTitle: "Lo último en el mundo de React Native", + theLatestInReactNative: + "Estamos aquí para mantenerte al día con todo lo que React Native tiene para ofrecer.", + reactNativeRadioLink: "React Native Radio", + reactNativeNewsletterLink: "Newsletter de React Native", + reactNativeLiveLink: "React Native Live", + chainReactConferenceLink: "Conferencia Chain React", + hireUsTitle: "Trabaja con Infinite Red en tu próximo proyecto", + hireUs: + "Ya sea para gestionar un proyecto de inicio a fin o educación a equipos a través de nuestros cursos y capacitación práctica, Infinite Red puede ayudarte en casi cualquier proyecto de React Native.", + hireUsLink: "Envíanos un mensaje", + }, + demoShowroomScreen: { + jumpStart: "Componentes para comenzar tu proyecto", + lorem2Sentences: + "Nulla cupidatat deserunt amet quis aliquip nostrud do adipisicing. Adipisicing excepteur elit laborum Lorem adipisicing do duis.", + demoHeaderTxExample: "Yay", + demoViaTxProp: "A través de el atributo `tx`", + demoViaSpecifiedTxProp: "A través de el atributo específico `{{prop}}Tx`", + }, + demoDebugScreen: { + howTo: "CÓMO HACERLO", + title: "Debug", + tagLine: + "Felicidades, aquí tienes una propuesta de arquitectura y base de código avanzada para una app en React Native. ¡Disfrutalos!", + reactotron: "Enviar a Reactotron", + reportBugs: "Reportar errores", + demoList: "Lista demo", + demoPodcastList: "Lista demo de podcasts", + androidReactotronHint: + "Si esto no funciona, asegúrate de que la app de escritorio de Reactotron se esté ejecutando, corre adb reverse tcp:9090 tcp:9090 desde tu terminal, y luego recarga la app.", + iosReactotronHint: + "Si esto no funciona, asegúrate de que la app de escritorio de Reactotron se esté ejecutando, y luego recarga la app.", + macosReactotronHint: + "Si esto no funciona, asegúrate de que la app de escritorio de Reactotron se esté ejecutando, y luego recarga la app.", + webReactotronHint: + "Si esto no funciona, asegúrate de que la app de escritorio de Reactotron se esté ejecutando, y luego recarga la app.", + windowsReactotronHint: + "Si esto no funciona, asegúrate de que la app de escritorio de Reactotron se esté ejecutando, y luego recarga la app.", + }, + demoPodcastListScreen: { + title: "Episodios de React Native Radio", + onlyFavorites: "Mostrar solo favoritos", + favoriteButton: "Favorito", + unfavoriteButton: "No favorito", + accessibility: { + cardHint: + "Haz doble clic para escuchar el episodio. Haz doble clic y mantén presionado para {{action}} este episodio.", + switch: "Activa para mostrar solo favoritos", + favoriteAction: "Cambiar a favorito", + favoriteIcon: "Episodio no favorito", + unfavoriteIcon: "Episodio favorito", + publishLabel: "Publicado el {{date}}", + durationLabel: "Duración: {{hours}} horas {{minutes}} minutos {{seconds}} segundos", + }, + noFavoritesEmptyState: { + heading: "Esto está un poco vacío", + content: + "No se han agregado episodios favoritos todavía. ¡Presiona el corazón dentro de un episodio para agregarlo a tus favoritos!", + }, + }, + + ...demoEs, +} + +export default es diff --git a/app/i18n/fr.ts b/app/i18n/fr.ts new file mode 100644 index 00000000..b482c28a --- /dev/null +++ b/app/i18n/fr.ts @@ -0,0 +1,131 @@ +import demoFr from "./demo-fr" +import { Translations } from "./en" + +const fr: Translations = { + common: { + ok: "OK !", + cancel: "Annuler", + back: "Retour", + logOut: "Déconnexion", + }, + welcomeScreen: { + postscript: + "psst — Ce n'est probablement pas à quoi ressemble votre application. (À moins que votre designer ne vous ait donné ces écrans, dans ce cas, mettez la en prod !)", + readyForLaunch: "Votre application, presque prête pour le lancement !", + exciting: "(ohh, c'est excitant !)", + letsGo: "Allons-y !", + }, + errorScreen: { + title: "Quelque chose s'est mal passé !", + friendlySubtitle: + "C'est l'écran que vos utilisateurs verront en production lorsqu'une erreur sera lancée. Vous voudrez personnaliser ce message (situé dans `app/i18n/fr.ts`) et probablement aussi la mise en page (`app/screens/ErrorScreen`). Si vous voulez le supprimer complètement, vérifiez `app/app.tsx` pour le composant .", + reset: "RÉINITIALISER L'APPLICATION", + traceTitle: "Erreur depuis %{name}", + }, + emptyStateComponent: { + generic: { + heading: "Si vide... si triste", + content: + "Aucune donnée trouvée pour le moment. Essayez de cliquer sur le bouton pour rafraîchir ou recharger l'application.", + button: "Essayons à nouveau", + }, + }, + + errors: { + invalidEmail: "Adresse e-mail invalide.", + }, + loginScreen: { + logIn: "Se connecter", + enterDetails: + "Entrez vos informations ci-dessous pour débloquer des informations top secrètes. Vous ne devinerez jamais ce que nous avons en attente. Ou peut-être que vous le ferez ; ce n'est pas de la science spatiale ici.", + emailFieldLabel: "E-mail", + passwordFieldLabel: "Mot de passe", + emailFieldPlaceholder: "Entrez votre adresse e-mail", + passwordFieldPlaceholder: "Mot de passe super secret ici", + tapToLogIn: "Appuyez pour vous connecter!", + hint: "Astuce : vous pouvez utiliser n'importe quelle adresse e-mail et votre mot de passe préféré :)", + }, + demoNavigator: { + componentsTab: "Composants", + debugTab: "Débogage", + communityTab: "Communauté", + podcastListTab: "Podcasts", + }, + demoCommunityScreen: { + title: "Connectez-vous avec la communauté", + tagLine: + "Rejoignez la communauté d'ingénieurs React Native d'Infinite Red et améliorez votre développement d'applications avec nous !", + joinUsOnSlackTitle: "Rejoignez-nous sur Slack", + joinUsOnSlack: + "Vous souhaitez vous connecter avec des ingénieurs React Native du monde entier ? Rejoignez la conversation dans la communauté Slack d'Infinite Red ! Notre communauté en pleine croissance est un espace sûr pour poser des questions, apprendre des autres et développer votre réseau.", + joinSlackLink: "Rejoindre la communauté Slack", + makeIgniteEvenBetterTitle: "Rendre Ignite encore meilleur", + makeIgniteEvenBetter: + "Vous avez une idée pour rendre Ignite encore meilleur ? Nous sommes heureux de l'entendre ! Nous cherchons toujours des personnes qui veulent nous aider à construire les meilleurs outils React Native. Rejoignez-nous sur GitHub pour nous aider à construire l'avenir d'Ignite.", + contributeToIgniteLink: "Contribuer à Ignite", + theLatestInReactNativeTitle: "Les dernières nouvelles de React Native", + theLatestInReactNative: + "Nous sommes là pour vous tenir au courant de tout ce que React Native a à offrir.", + reactNativeRadioLink: "React Native Radio", + reactNativeNewsletterLink: "React Native Newsletter", + reactNativeLiveLink: "React Native Live", + chainReactConferenceLink: "Conférence Chain React", + hireUsTitle: "Engagez Infinite Red pour votre prochain projet", + hireUs: + "Que ce soit pour gérer un projet complet ou pour former des équipes à notre formation pratique, Infinite Red peut vous aider pour presque tous les projets React Native.", + hireUsLink: "Envoyez-nous un message", + }, + demoShowroomScreen: { + jumpStart: "Composants pour démarrer votre projet !", + lorem2Sentences: + "Nulla cupidatat deserunt amet quis aliquip nostrud do adipisicing. Adipisicing excepteur elit laborum Lorem adipisicing do duis.", + demoHeaderTxExample: "Yay", + demoViaTxProp: "Via la propriété `tx`", + demoViaSpecifiedTxProp: "Via la propriété `{{prop}}Tx` spécifiée", + }, + demoDebugScreen: { + howTo: "COMMENT FAIRE", + title: "Débugage", + tagLine: + "Félicitations, vous avez un modèle d'application React Native très avancé ici. Profitez de cette base de code !", + reactotron: "Envoyer à Reactotron", + reportBugs: "Signaler des bugs", + demoList: "Liste de démonstration", + demoPodcastList: "Liste de podcasts de démonstration", + androidReactotronHint: + "Si cela ne fonctionne pas, assurez-vous que l'application de bureau Reactotron est en cours d'exécution, exécutez adb reverse tcp:9090 tcp:9090 à partir de votre terminal, puis rechargez l'application.", + iosReactotronHint: + "Si cela ne fonctionne pas, assurez-vous que l'application de bureau Reactotron est en cours d'exécution, puis rechargez l'application.", + macosReactotronHint: + "Si cela ne fonctionne pas, assurez-vous que l'application de bureau Reactotron est en cours d'exécution, puis rechargez l'application.", + webReactotronHint: + "Si cela ne fonctionne pas, assurez-vous que l'application de bureau Reactotron est en cours d'exécution, puis rechargez l'application.", + windowsReactotronHint: + "Si cela ne fonctionne pas, assurez-vous que l'application de bureau Reactotron est en cours d'exécution, puis rechargez l'application.", + }, + demoPodcastListScreen: { + title: "Épisodes de Radio React Native", + onlyFavorites: "Afficher uniquement les favoris", + favoriteButton: "Favori", + unfavoriteButton: "Non favori", + accessibility: { + cardHint: + "Double-cliquez pour écouter l'épisode. Double-cliquez et maintenez pour {{action}} cet épisode.", + switch: "Activez pour afficher uniquement les favoris", + favoriteAction: "Basculer en favori", + favoriteIcon: "Épisode non favori", + unfavoriteIcon: "Épisode favori", + publishLabel: "Publié le {{date}}", + durationLabel: "Durée : {{hours}} heures {{minutes}} minutes {{seconds}} secondes", + }, + noFavoritesEmptyState: { + heading: "C'est un peu vide ici", + content: + "Aucun favori n'a été ajouté pour le moment. Appuyez sur le cœur d'un épisode pour l'ajouter à vos favoris !", + }, + }, + + ...demoFr, +} + +export default fr diff --git a/app/i18n/hi.ts b/app/i18n/hi.ts new file mode 100644 index 00000000..3694701b --- /dev/null +++ b/app/i18n/hi.ts @@ -0,0 +1,129 @@ +import demoHi from "./demo-hi" +import { Translations } from "./en" + +const hi: Translations = { + common: { + ok: "ठीक है!", + cancel: "रद्द करें", + back: "वापस", + logOut: "लॉग आउट", + }, + welcomeScreen: { + postscript: + "psst - शायद आपका ऐप ऐसा नहीं दिखता है। (जब तक कि आपके डिजाइनर ने आपको ये स्क्रीन नहीं दी हों, और उस स्थिति में, इसे लॉन्च करें!)", + readyForLaunch: "आपका ऐप, लगभग लॉन्च के लिए तैयार है!", + exciting: "(ओह, यह रोमांचक है!)", + letsGo: "चलो चलते हैं!", + }, + errorScreen: { + title: "कुछ गलत हो गया!", + friendlySubtitle: + "यह वह स्क्रीन है जो आपके उपयोगकर्ता संचालन में देखेंगे जब कोई त्रुटि होगी। आप इस संदेश को बदलना चाहेंगे (जो `app/i18n/hi.ts` में स्थित है) और शायद लेआउट भी (`app/screens/ErrorScreen`)। यदि आप इसे पूरी तरह से हटाना चाहते हैं, तो `app/app.tsx` में कंपोनेंट की जांच करें।", + reset: "ऐप रीसेट करें", + traceTitle: "%{name} स्टैक से त्रुटि", + }, + emptyStateComponent: { + generic: { + heading: "इतना खाली... इतना उदास", + content: "अभी तक कोई डेटा नहीं मिला। रीफ्रेश करने या ऐप को पुनः लोड करने के लिए बटन दबाएं।", + button: "चलो फिर से कोशिश करते हैं", + }, + }, + + errors: { + invalidEmail: "अमान्य ईमेल पता।", + }, + loginScreen: { + logIn: "लॉग इन करें", + enterDetails: + "सर्वश्रेष्ठ रहस्य पता करने के लिए नीचे अपना विवरण दर्ज करें। आप कभी अनुमान नहीं लगा पाएंगे कि हमारे पास क्या इंतजार कर रहा है। या शायद आप कर सकते हैं; यह रॉकेट साइंस नहीं है।", + emailFieldLabel: "ईमेल", + passwordFieldLabel: "पासवर्ड", + emailFieldPlaceholder: "अपना ईमेल पता दर्ज करें", + passwordFieldPlaceholder: "सुपर सीक्रेट पासवर्ड यहाँ", + tapToLogIn: "लॉग इन करने के लिए टैप करें!", + hint: "संकेत: आप किसी भी ईमेल पते और अपने पसंदीदा पासवर्ड का उपयोग कर सकते हैं :)", + }, + demoNavigator: { + componentsTab: "कंपोनेंट्स", + debugTab: "डीबग", + communityTab: "समुदाय", + podcastListTab: "पॉडकास्ट", + }, + demoCommunityScreen: { + title: "समुदाय से जुड़ें", + tagLine: + "Infinite Red के React Native इंजीनियरों के समुदाय से जुड़ें और हमारे साथ अपने ऐप विकास को बेहतर बनाएं!", + joinUsOnSlackTitle: "Slack पर हमसे जुड़ें", + joinUsOnSlack: + "क्या आप चाहते हैं कि दुनिया भर के React Native इंजीनियरों से जुड़ने के लिए कोई जगह हो? Infinite Red Community Slack में बातचीत में शामिल हों! हमारा बढ़ता हुआ समुदाय प्रश्न पूछने, दूसरों से सीखने और अपने नेटवर्क को बढ़ाने के लिए एक सुरक्षित स्थान है।", + joinSlackLink: "Slack समुदाय में शामिल हों", + makeIgniteEvenBetterTitle: "Ignite को और बेहतर बनाएं", + makeIgniteEvenBetter: + "Ignite को और बेहतर बनाने का कोई विचार है? हमें यह सुनकर खुशी होगी! हम हमेशा ऐसे लोगों की तलाश में रहते हैं जो हमें सर्वश्रेष्ठ React Native टूलिंग बनाने में मदद करना चाहते हैं। Ignite के भविष्य को बनाने में हमारे साथ शामिल होने के लिए GitHub पर हमसे जुड़ें।", + contributeToIgniteLink: "Ignite में योगदान दें", + theLatestInReactNativeTitle: "React Native में नवीनतम", + theLatestInReactNative: "हम आपको React Native के सभी प्रस्तावों पर अपडेट रखने के लिए यहां हैं।", + reactNativeRadioLink: "React Native रेडियो", + reactNativeNewsletterLink: "React Native न्यूजलेटर", + reactNativeLiveLink: "React Native लाइव", + chainReactConferenceLink: "Chain React कॉन्फ्रेंस", + hireUsTitle: "अपने अगले प्रोजेक्ट के लिए Infinite Red को काम पर रखें", + hireUs: + "चाहे वह एक पूरा प्रोजेक्ट चलाना हो या हमारे हैंड्स-ऑन प्रशिक्षण के साथ टीमों को गति देना हो, Infinite Red लगभग किसी भी React Native प्रोजेक्ट में मदद कर सकता है।", + hireUsLink: "हमें एक संदेश भेजें", + }, + demoShowroomScreen: { + jumpStart: "अपने प्रोजेक्ट को जंप स्टार्ट करने के लिए कंपोनेंट्स!", + lorem2Sentences: + "कोई भी काम जो आप नहीं करना चाहते, उसे करने के लिए किसी और को ढूंढना चाहिए। जो लोग दूसरों की मदद करते हैं, वे खुद की भी मदद करते हैं।", + demoHeaderTxExample: "हाँ", + demoViaTxProp: "`tx` प्रॉप के माध्यम से", + demoViaSpecifiedTxProp: "`{{prop}}Tx` प्रॉप के माध्यम से", + }, + demoDebugScreen: { + howTo: "कैसे करें", + title: "डीबग", + tagLine: + "बधाई हो, आपके पास यहां एक बहुत उन्नत React Native ऐप टेम्पलेट है। इस बॉयलरप्लेट का लाभ उठाएं!", + reactotron: "Reactotron को भेजें", + reportBugs: "बग्स की रिपोर्ट करें", + demoList: "डेमो सूची", + demoPodcastList: "डेमो पॉडकास्ट सूची", + androidReactotronHint: + "यदि यह काम नहीं करता है, तो सुनिश्चित करें कि Reactotron डेस्कटॉप ऐप चल रहा है, अपने टर्मिनल से adb reverse tcp:9090 tcp:9090 चलाएं, और ऐप को पुनः लोड करें।", + iosReactotronHint: + "यदि यह काम नहीं करता है, तो सुनिश्चित करें कि Reactotron डेस्कटॉप ऐप चल रहा है और ऐप को पुनः लोड करें।", + macosReactotronHint: + "यदि यह काम नहीं करता है, तो सुनिश्चित करें कि Reactotron डेस्कटॉप ऐप चल रहा है और ऐप को पुनः लोड करें।", + webReactotronHint: + "यदि यह काम नहीं करता है, तो सुनिश्चित करें कि Reactotron डेस्कटॉप ऐप चल रहा है और ऐप को पुनः लोड करें।", + windowsReactotronHint: + "यदि यह काम नहीं करता है, तो सुनिश्चित करें कि Reactotron डेस्कटॉप ऐप चल रहा है और ऐप को पुनः लोड करें।", + }, + demoPodcastListScreen: { + title: "React Native रेडियो एपिसोड", + onlyFavorites: "केवल पसंदीदा दिखाएं", + favoriteButton: "पसंदीदा", + unfavoriteButton: "नापसंद", + accessibility: { + cardHint: + "एपिसोड सुनने के लिए डबल टैप करें। इस एपिसोड को {{action}} करने के लिए डबल टैप करें और होल्ड करें।", + switch: "केवल पसंदीदा दिखाने के लिए स्विच करें", + favoriteAction: "पसंदीदा टॉगल करें", + favoriteIcon: "एपिसोड पसंदीदा नहीं है", + unfavoriteIcon: "एपिसोड पसंदीदा है", + publishLabel: "{{date}} को प्रकाशित", + durationLabel: "अवधि: {{hours}} घंटे {{minutes}} मिनट {{seconds}} सेकंड", + }, + noFavoritesEmptyState: { + heading: "यह थोड़ा खाली लगता है", + content: + "अभी तक कोई पसंदीदा नहीं जोड़ा गया है। इसे अपने पसंदीदा में जोड़ने के लिए किसी एपिसोड पर दिल पर टैप करें!", + }, + }, + + ...demoHi, +} + +export default hi diff --git a/app/i18n/i18n.ts b/app/i18n/i18n.ts new file mode 100644 index 00000000..0cc03f50 --- /dev/null +++ b/app/i18n/i18n.ts @@ -0,0 +1,86 @@ +import * as Localization from "expo-localization" +import { I18nManager } from "react-native" +import i18n from "i18next" +import { initReactI18next } from "react-i18next" +import "intl-pluralrules" + +// if English isn't your default language, move Translations to the appropriate language file. +import en, { Translations } from "./en" +import ar from "./ar" +import ko from "./ko" +import es from "./es" +import fr from "./fr" +import ja from "./ja" +import hi from "./hi" + +const fallbackLocale = "en-US" + +const systemLocales = Localization.getLocales() + +const resources = { ar, en, ko, es, fr, ja, hi } +const supportedTags = Object.keys(resources) + +// Checks to see if the device locale matches any of the supported locales +// Device locale may be more specific and still match (e.g., en-US matches en) +const systemTagMatchesSupportedTags = (deviceTag: string) => { + const primaryTag = deviceTag.split("-")[0] + return supportedTags.includes(primaryTag) +} + +const pickSupportedLocale: () => Localization.Locale | undefined = () => { + return systemLocales.find((locale) => systemTagMatchesSupportedTags(locale.languageTag)) +} + +const locale = pickSupportedLocale() + +export let isRTL = false + +// Need to set RTL ASAP to ensure the app is rendered correctly. Waiting for i18n to init is too late. +if (locale?.languageTag && locale?.textDirection === "rtl") { + I18nManager.allowRTL(true) + isRTL = true +} else { + I18nManager.allowRTL(false) +} + +export const initI18n = async () => { + i18n.use(initReactI18next) + + await i18n.init({ + resources, + lng: locale?.languageTag ?? fallbackLocale, + fallbackLng: fallbackLocale, + interpolation: { + escapeValue: false, + }, + }) + + return i18n +} + +/** + * Builds up valid keypaths for translations. + */ + +export type TxKeyPath = RecursiveKeyOf + +// via: https://stackoverflow.com/a/65333050 +type RecursiveKeyOf = { + [TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue +}[keyof TObj & (string | number)] + +type RecursiveKeyOfInner = { + [TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue +}[keyof TObj & (string | number)] + +type RecursiveKeyOfHandleValue< + TValue, + Text extends string, + IsFirstLevel extends boolean, +> = TValue extends any[] + ? Text + : TValue extends object + ? IsFirstLevel extends true + ? Text | `${Text}:${RecursiveKeyOfInner}` + : Text | `${Text}.${RecursiveKeyOfInner}` + : Text diff --git a/app/i18n/index.ts b/app/i18n/index.ts new file mode 100644 index 00000000..d4f13b3a --- /dev/null +++ b/app/i18n/index.ts @@ -0,0 +1,4 @@ +import "./i18n" + +export * from "./i18n" +export * from "./translate" diff --git a/app/i18n/ja.ts b/app/i18n/ja.ts new file mode 100644 index 00000000..e6698963 --- /dev/null +++ b/app/i18n/ja.ts @@ -0,0 +1,129 @@ +import demoJa from "./demo-ja" +import { Translations } from "./en" + +const ja: Translations = { + common: { + ok: "OK", + cancel: "キャンセル", + back: "戻る", + logOut: "ログアウト", + }, + welcomeScreen: { + postscript: + "注目! — このアプリはお好みの見た目では無いかもしれません(デザイナーがこのスクリーンを送ってこない限りは。もしそうなら公開しちゃいましょう!)", + readyForLaunch: "このアプリはもう少しで公開できます!", + exciting: "(楽しみですね!)", + letsGo: "レッツゴー!", + }, + errorScreen: { + title: "問題が発生しました", + friendlySubtitle: + "本番では、エラーが投げられた時にこのページが表示されます。もし使うならこのメッセージに変更を加えてください(`app/i18n/jp.ts`)レイアウトはこちらで変更できます(`app/screens/ErrorScreen`)。もしこのスクリーンを取り除きたい場合は、`app/app.tsx`にあるコンポーネントをチェックしてください", + reset: "リセット", + traceTitle: "エラーのスタック: %{name}", + }, + emptyStateComponent: { + generic: { + heading: "静かだ...悲しい。", + content: + "データが見つかりません。ボタンを押してアプリをリロード、またはリフレッシュしてください。", + button: "もう一度やってみよう", + }, + }, + + errors: { + invalidEmail: "有効なメールアドレスを入力してください.", + }, + loginScreen: { + logIn: "ログイン", + enterDetails: + "ここにあなたの情報を入力してトップシークレットをアンロックしましょう。何が待ち構えているか予想もつかないはずです。はたまたそうでも無いかも - ロケットサイエンスほど複雑なものではありません。", + emailFieldLabel: "メールアドレス", + passwordFieldLabel: "パスワード", + emailFieldPlaceholder: "メールアドレスを入力してください", + passwordFieldPlaceholder: "パスワードを入力してください", + tapToLogIn: "タップしてログインしよう!", + hint: "ヒント: お好みのメールアドレスとパスワードを使ってください :)", + }, + demoNavigator: { + componentsTab: "コンポーネント", + debugTab: "デバッグ", + communityTab: "コミュニティ", + podcastListTab: "ポッドキャスト", + }, + demoCommunityScreen: { + title: "コミュニティと繋がろう", + tagLine: + "Infinite RedのReact Nativeエンジニアコミュニティに接続して、一緒にあなたのアプリ開発をレベルアップしましょう!", + joinUsOnSlackTitle: "私たちのSlackに参加しましょう", + joinUsOnSlack: + "世界中のReact Nativeエンジニアと繋がりたいを思いませんか?Infinite RedのコミュニティSlackに参加しましょう!私達のコミュニティは安全に質問ができ、お互いから学び、あなたのネットワークを広げることができます。", + joinSlackLink: "Slackコミュニティに参加する", + makeIgniteEvenBetterTitle: "Igniteをより良くする", + makeIgniteEvenBetter: + "Igniteをより良くする為のアイデアはありますか? そうであれば聞きたいです! 私たちはいつでも最良のReact Nativeのツールを開発する為に助けを求めています。GitHubで私たちと一緒にIgniteの未来を作りましょう。", + contributeToIgniteLink: "Igniteにコントリビュートする", + theLatestInReactNativeTitle: "React Nativeの今", + theLatestInReactNative: "React Nativeの現在をあなたにお届けします。", + reactNativeRadioLink: "React Native Radio", + reactNativeNewsletterLink: "React Native Newsletter", + reactNativeLiveLink: "React Native Live", + chainReactConferenceLink: "Chain React Conference", + hireUsTitle: "あなたの次のプロジェクトでInfinite Redと契約する", + hireUs: + "それがプロジェクト全体でも、チームにトレーニングをしてあげたい時でも、Infinite RedはReact Nativeのことであればなんでもお手伝いができます。", + hireUsLink: "メッセージを送る", + }, + demoShowroomScreen: { + jumpStart: "あなたのプロジェクトをスタートさせるコンポーネントです!", + lorem2Sentences: + "Nulla cupidatat deserunt amet quis aliquip nostrud do adipisicing. Adipisicing excepteur elit laborum Lorem adipisicing do duis.", + demoHeaderTxExample: "Yay", + demoViaTxProp: "`tx`から", + demoViaSpecifiedTxProp: "`{{prop}}Tx`から", + }, + demoDebugScreen: { + howTo: "ハウツー", + title: "デバッグ", + tagLine: + "おめでとうございます、あなたはとてもハイレベルなReact Nativeのテンプレートを使ってます。このボイラープレートを活用してください!", + reactotron: "Reactotronに送る", + reportBugs: "バグをレポートする", + demoList: "デモリスト", + demoPodcastList: "デモのポッドキャストリスト", + androidReactotronHint: + "もし動かなければ、Reactotronのデスクトップアプリが実行されていることを確認して, このコマンドをターミナルで実行した後、アプリをアプリをリロードしてください。 adb reverse tcp:9090 tcp:9090", + iosReactotronHint: + "もし動かなければ、Reactotronのデスクトップアプリが実行されていることを確認して、アプリをリロードしてください。", + macosReactotronHint: + "もし動かなければ、Reactotronのデスクトップアプリが実行されていることを確認して、アプリをリロードしてください。", + webReactotronHint: + "もし動かなければ、Reactotronのデスクトップアプリが実行されていることを確認して、アプリをリロードしてください。", + windowsReactotronHint: + "もし動かなければ、Reactotronのデスクトップアプリが実行されていることを確認して、アプリをリロードしてください。", + }, + demoPodcastListScreen: { + title: "React Native Radioのエピソード", + onlyFavorites: "お気に入り表示", + favoriteButton: "お気に入り", + unfavoriteButton: "お気に入りを外す", + accessibility: { + cardHint: "ダブルタップで再生します。 ダブルタップと長押しで {{action}}", + switch: "スイッチオンでお気に入りを表示する", + favoriteAction: "お気に入りの切り替え", + favoriteIcon: "お気に入りのエピソードではありません", + unfavoriteIcon: "お気に入りのエピソードです", + publishLabel: "公開日 {{date}}", + durationLabel: "再生時間: {{hours}} 時間 {{minutes}} 分 {{seconds}} 秒", + }, + noFavoritesEmptyState: { + heading: "どうやら空っぽのようですね", + content: + "お気に入りのエピソードがまだありません。エピソードにあるハートマークにタップして、お気に入りに追加しましょう!", + }, + }, + + ...demoJa, +} + +export default ja diff --git a/app/i18n/ko.ts b/app/i18n/ko.ts new file mode 100644 index 00000000..2403206a --- /dev/null +++ b/app/i18n/ko.ts @@ -0,0 +1,128 @@ +import demoKo from "./demo-ko" +import { Translations } from "./en" + +const ko: Translations = { + common: { + ok: "확인!", + cancel: "취소", + back: "뒤로", + logOut: "로그아웃", + }, + welcomeScreen: { + postscript: + "잠깐! — 지금 보시는 것은 아마도 당신의 앱의 모양새가 아닐겁니다. (디자이너분이 이렇게 건내주셨다면 모를까요. 만약에 그렇다면, 이대로 가져갑시다!) ", + readyForLaunch: "출시 준비가 거의 끝난 나만의 앱!", + exciting: "(오, 이거 신나는데요!)", + letsGo: "가보자구요!", + }, + errorScreen: { + title: "뭔가 잘못되었습니다!", + friendlySubtitle: + "이 화면은 오류가 발생할 때 프로덕션에서 사용자에게 표시됩니다. 이 메시지를 커스터마이징 할 수 있고(해당 파일은 `app/i18n/ko.ts` 에 있습니다) 레이아웃도 마찬가지로 수정할 수 있습니다(`app/screens/error`). 만약 이 오류화면을 완전히 없에버리고 싶다면 `app/app.tsx` 파일에서 컴포넌트를 확인하기 바랍니다.", + reset: "초기화", + traceTitle: "%{name} 스택에서의 오류", + }, + emptyStateComponent: { + generic: { + heading: "너무 텅 비어서.. 너무 슬퍼요..", + content: "데이터가 없습니다. 버튼을 눌러서 리프레쉬 하시거나 앱을 리로드하세요.", + button: "다시 시도해봅시다", + }, + }, + + errors: { + invalidEmail: "잘못된 이메일 주소 입니다.", + }, + loginScreen: { + logIn: "로그인", + enterDetails: + "일급비밀 정보를 해제하기 위해 상세 정보를 입력하세요. 무엇이 기다리고 있는지 절대 모를겁니다. 혹은 알 수 있을지도 모르겠군요. 엄청 복잡한 뭔가는 아닙니다.", + emailFieldLabel: "이메일", + passwordFieldLabel: "비밀번호", + emailFieldPlaceholder: "이메일을 입력하세요", + passwordFieldPlaceholder: "엄청 비밀스러운 암호를 입력하세요", + tapToLogIn: "눌러서 로그인 하기!", + hint: "힌트: 가장 좋아하는 암호와 아무런 아무 이메일 주소나 사용할 수 있어요 :)", + }, + demoNavigator: { + componentsTab: "컴포넌트", + debugTab: "디버그", + communityTab: "커뮤니티", + podcastListTab: "팟캐스트", + }, + demoCommunityScreen: { + title: "커뮤니티와 함께해요", + tagLine: + "전문적인 React Native 엔지니어들로 구성된 Infinite Red 커뮤니티에 접속해서 함께 개발 실력을 향상시켜 보세요!", + joinUsOnSlackTitle: "Slack 에 참여하세요", + joinUsOnSlack: + "전 세계 React Native 엔지니어들과 함께할 수 있는 곳이 있었으면 좋겠죠? Infinite Red Community Slack 에서 대화에 참여하세요! 우리의 성장하는 커뮤니티는 질문을 던지고, 다른 사람들로부터 배우고, 네트워크를 확장할 수 있는 안전한 공간입니다. ", + joinSlackLink: "Slack 에 참여하기", + makeIgniteEvenBetterTitle: "Ignite 을 향상시켜요", + makeIgniteEvenBetter: + "Ignite 을 더 좋게 만들 아이디어가 있나요? 기쁜 소식이네요. 우리는 항상 최고의 React Native 도구를 구축하는데 도움을 줄 수 있는 분들을 찾고 있습니다. GitHub 에서 Ignite 의 미래를 만들어 가는것에 함께해 주세요.", + contributeToIgniteLink: "Ignite 에 기여하기", + theLatestInReactNativeTitle: "React Native 의 최신정보", + theLatestInReactNative: "React Native 가 제공하는 모든 최신 정보를 알려드립니다.", + reactNativeRadioLink: "React Native 라디오", + reactNativeNewsletterLink: "React Native 뉴스레터", + reactNativeLiveLink: "React Native 라이브 스트리밍", + chainReactConferenceLink: "Chain React 컨퍼런스", + hireUsTitle: "다음 프로젝트에 Infinite Red 를 고용하세요", + hireUs: + "프로젝트 전체를 수행하든, 실무 교육을 통해 팀의 개발 속도에 박차를 가하든 상관없이, Infinite Red 는 React Native 프로젝트의 모든 분야의 에서 도움을 드릴 수 있습니다.", + hireUsLink: "메세지 보내기", + }, + demoShowroomScreen: { + jumpStart: "프로젝트를 바로 시작할 수 있는 컴포넌트들!", + lorem2Sentences: + "별 하나에 추억과, 별 하나에 사랑과, 별 하나에 쓸쓸함과, 별 하나에 동경(憧憬)과, 별 하나에 시와, 별 하나에 어머니, 어머니", + demoHeaderTxExample: "야호", + demoViaTxProp: "`tx` Prop 을 통해", + demoViaSpecifiedTxProp: "`{{prop}}Tx` Prop 을 통해", + }, + demoDebugScreen: { + howTo: "사용방법", + title: "디버그", + tagLine: + "축하합니다. 여기 아주 고급스러운 React Native 앱 템플릿이 있습니다. 이 보일러 플레이트를 사용해보세요!", + reactotron: "Reactotron 으로 보내기", + reportBugs: "버그 보고하기", + demoList: "데모 목록", + demoPodcastList: "데모 팟캐스트 목록", + androidReactotronHint: + "만약에 동작하지 않는 경우, Reactotron 데스크탑 앱이 실행중인지 확인 후, 터미널에서 adb reverse tcp:9090 tcp:9090 을 실행한 다음 앱을 다시 실행해보세요.", + iosReactotronHint: + "만약에 동작하지 않는 경우, Reactotron 데스크탑 앱이 실행중인지 확인 후 앱을 다시 실행해보세요.", + macosReactotronHint: + "만약에 동작하지 않는 경우, Reactotron 데스크탑 앱이 실행중인지 확인 후 앱을 다시 실행해보세요.", + webReactotronHint: + "만약에 동작하지 않는 경우, Reactotron 데스크탑 앱이 실행중인지 확인 후 앱을 다시 실행해보세요.", + windowsReactotronHint: + "만약에 동작하지 않는 경우, Reactotron 데스크탑 앱이 실행중인지 확인 후 앱을 다시 실행해보세요.", + }, + demoPodcastListScreen: { + title: "React Native 라디오 에피소드", + onlyFavorites: "즐겨찾기만 보기", + favoriteButton: "즐겨찾기", + unfavoriteButton: "즐겨찾기 해제", + accessibility: { + cardHint: + "에피소드를 들으려면 두 번 탭하세요. 이 에피소드를 좋아하거나 싫어하려면 두 번 탭하고 길게 누르세요.", + switch: "즐겨찾기를 사용하려면 스위치를 사용하세요.", + favoriteAction: "즐겨찾기 토글", + favoriteIcon: "좋아하는 에피소드", + unfavoriteIcon: "즐겨찾기하지 않은 에피소드", + publishLabel: "{{date}} 에 발행됨", + durationLabel: "소요시간: {{hours}}시간 {{minutes}}분 {{seconds}}초", + }, + noFavoritesEmptyState: { + heading: "조금 텅 비어 있네요.", + content: "즐겨찾기가 없습니다. 에피소드에 있는 하트를 눌러서 즐겨찾기에 추가하세요.", + }, + }, + + ...demoKo, +} + +export default ko diff --git a/app/i18n/translate.ts b/app/i18n/translate.ts new file mode 100644 index 00000000..22f99984 --- /dev/null +++ b/app/i18n/translate.ts @@ -0,0 +1,32 @@ +import i18n from "i18next" +import type { TOptions } from "i18next" +import { TxKeyPath } from "./i18n" + +/** + * Translates text. + * @param {TxKeyPath} key - The i18n key. + * @param {TOptions} options - The i18n options. + * @returns {string} - The translated text. + * @example + * Translations: + * + * ```en.ts + * { + * "hello": "Hello, {{name}}!" + * } + * ``` + * + * Usage: + * ```ts + * import { translate } from "./i18n" + * + * translate("common:ok", { name: "world" }) + * // => "Hello world!" + * ``` + */ +export function translate(key: TxKeyPath, options?: TOptions): string { + if (i18n.isInitialized) { + return i18n.t(key, options) + } + return key +} diff --git a/app/models/RootStore.ts b/app/models/RootStore.ts index 37c8c74c..435920ed 100644 --- a/app/models/RootStore.ts +++ b/app/models/RootStore.ts @@ -1,23 +1,16 @@ import { Instance, SnapshotIn, SnapshotOut, types } from "mobx-state-tree" -import { ChatStoreWithActions } from "./chat/ChatStore" -import { ToolStoreModel } from "./tools" +import { ChatStoreModel } from "./chat/ChatStore" import { CoderStoreModel } from "./coder/CoderStore" const RootStoreModel = types .model("RootStore") .props({ - chatStore: types.optional(ChatStoreWithActions, {}), - toolStore: types.optional(ToolStoreModel, { - tools: [], - isInitialized: false, - error: null, - }), + chatStore: types.optional(ChatStoreModel, {}), coderStore: types.optional(CoderStoreModel, { - isInitialized: false, error: null, githubToken: "", repos: [], - activeRepo: null, + activeRepoIndex: null, }) }) @@ -31,16 +24,10 @@ export type RootStore = Instance export const createRootStoreDefaultModel = () => RootStoreModel.create({ chatStore: {}, - toolStore: { - tools: [], - isInitialized: false, - error: null, - }, coderStore: { - isInitialized: false, error: null, githubToken: "", repos: [], - activeRepo: null, + activeRepoIndex: null, } - }) \ No newline at end of file + }) diff --git a/app/models/_helpers/setupRootStore.ts b/app/models/_helpers/setupRootStore.ts index f7a5eaaa..faa152e1 100644 --- a/app/models/_helpers/setupRootStore.ts +++ b/app/models/_helpers/setupRootStore.ts @@ -1,5 +1,4 @@ -import { applySnapshot, IDisposer, onSnapshot } from "mobx-state-tree" -import { log } from "@/utils/log" +import { applySnapshot, onSnapshot } from "mobx-state-tree" import * as storage from "../../utils/storage" import { RootStore, RootStoreSnapshotIn } from "../RootStore" @@ -25,31 +24,6 @@ export async function setupRootStore(rootStore: RootStore) { } } - // Initialize tools immediately after restoring state - try { - log({ - name: "[setupRootStore] Initializing tools", - preview: "Starting tool initialization", - value: { isInitialized: rootStore.toolStore.isInitialized }, - important: true, - }) - - // Force initialize tools regardless of isInitialized state - await rootStore.toolStore.initializeDefaultTools() - - log({ - name: "[setupRootStore] Tools initialized", - preview: "Tool initialization complete", - value: { - tools: rootStore.toolStore.tools.map(t => t.id), - isInitialized: rootStore.toolStore.isInitialized - }, - important: true, - }) - } catch (e) { - log.error("[setupRootStore] Failed to initialize tools:", e) - } - // track changes & save to AsyncStorage const unsubscribe = onSnapshot(rootStore, (snapshot) => storage.save(ROOT_STATE_STORAGE_KEY, snapshot)) diff --git a/app/models/_helpers/useStores.ts b/app/models/_helpers/useStores.ts index 743131dd..d5490711 100644 --- a/app/models/_helpers/useStores.ts +++ b/app/models/_helpers/useStores.ts @@ -1,5 +1,7 @@ import { createContext, useContext, useEffect, useState } from "react" -import { RootStore, RootStoreModel, createRootStoreDefaultModel } from "../RootStore" +import { + createRootStoreDefaultModel, RootStore, RootStoreModel +} from "../RootStore" import { setupRootStore } from "./setupRootStore" /** @@ -34,35 +36,37 @@ export const useInitialRootStore = (callback?: () => void | Promise) => { // Kick off initial async loading actions, like loading fonts and rehydrating RootStore useEffect(() => { let _unsubscribe: () => void | undefined - ;(async () => { - try { - // set up the RootStore (returns the state restored from AsyncStorage) - const { unsubscribe } = await setupRootStore(rootStore) - _unsubscribe = unsubscribe + ; (async () => { + try { + // set up the RootStore (returns the state restored from AsyncStorage) + const { unsubscribe } = await setupRootStore(rootStore) + _unsubscribe = unsubscribe - // reactotron integration with the MST root store (DEV only) - if (__DEV__) { - // @ts-ignore - console.tron?.trackMstNode(rootStore) - } + // reactotron integration with the MST root store (DEV only) + if (__DEV__) { + // @ts-ignore + console.tron?.trackMstNode(rootStore) + } + + // rootStore.coderStore.setup() - // let the app know we've finished rehydrating - setRehydrated(true) + // let the app know we've finished rehydrating + setRehydrated(true) - // invoke the callback, if provided - if (callback) await callback() - } catch (error) { - console.error("Failed to setup root store:", error) - // Still set rehydrated to true to prevent infinite loading - setRehydrated(true) - } - })() + // invoke the callback, if provided + if (callback) await callback() + } catch (error) { + console.error("Failed to setup root store:", error) + // Still set rehydrated to true to prevent infinite loading + setRehydrated(true) + } + })() return () => { // cleanup if (_unsubscribe !== undefined) _unsubscribe() } - }, [callback]) + }, []) // Empty dependency array since we only want this to run once on mount return { rehydrated } -} \ No newline at end of file +} diff --git a/app/models/chat/ChatActions.ts b/app/models/chat/ChatActions.ts deleted file mode 100644 index 530aa008..00000000 --- a/app/models/chat/ChatActions.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { flow, Instance, getRoot } from "mobx-state-tree" -import { log } from "@/utils/log" -import { groqChatApi } from "../../services/groq/groq-chat" -import { geminiChatApi } from "../../services/gemini/gemini-chat" -import type { RootStore } from "../RootStore" -import type { ChatStore, ChatStoreModel } from "./ChatStore" -import type { ITool } from "../tools/ToolStore" -import { MessageModel } from "./ChatStore" - -type ChatActions = { - sendMessage: (content: string) => Promise -} - -/** - * Chat actions that integrate with the Groq and Gemini APIs - */ -export const withGroqActions = (self: Instance): ChatActions => ({ - /** - * Sends a message to the selected model and handles the response - */ - sendMessage: flow(function* (content: string): Generator { - try { - // Add user message - const userMessage = self.addMessage({ - role: "user", - content, - metadata: { - conversationId: self.currentConversationId, - }, - }) - - // Add placeholder assistant message - const assistantMessage = self.addMessage({ - role: "assistant", - content: "", - metadata: { - conversationId: self.currentConversationId, - isGenerating: true, - }, - }) - - self.setIsGenerating(true) - - let result - if (self.activeModel === "groq") { - // Get chat completion from Groq - result = yield groqChatApi.createChatCompletion( - self.currentMessages, - "llama-3.1-8b-instant", - { - temperature: 0.7, - max_tokens: 1024, - }, - ) - } else { - // Get enabled tools from store - const rootStore = getRoot(self) - const enabledTools: ITool[] = rootStore.toolStore.enabledTools - - // Get chat completion from Gemini - result = yield geminiChatApi.createChatCompletion( - self.currentMessages, - { - temperature: 0.7, - maxOutputTokens: 1024, - tools: enabledTools, - }, - ) - } - - if (result.kind === "ok") { - const response = result.response - const message = response.choices[0].message - const content = message.content - - // Try to parse content as a function call - let functionCall = null - try { - const parsed = JSON.parse(content) - if (parsed && parsed.functionCall && parsed.functionCall.name && parsed.functionCall.args) { - functionCall = parsed.functionCall - } - } catch (err) { - // Not a function call JSON - log({ - name: "[ChatActions] Not a function call JSON", - preview: "Parse error", - value: { content, error: err }, - important: true, - }) - } - - if (functionCall) { - log({ - name: "[ChatActions] Function Call", - preview: "Executing function", - value: { functionCall }, - important: true, - }) - - // Update message to show function call - self.updateMessage(assistantMessage.id, { - content: JSON.stringify(functionCall, null, 2), - metadata: { - ...assistantMessage.metadata, - isGenerating: true, - tokens: response.usage?.completion_tokens, - model: self.activeModel, - }, - }) - - try { - // Get the tool implementation - const rootStore = getRoot(self) - const toolId = `github_${functionCall.name}` - - log({ - name: "[ChatActions] Looking for tool", - preview: "Getting tool", - value: { - toolId, - tools: rootStore.toolStore.tools.map(t => ({ - id: t.id, - hasImplementation: !!t.implementation - })), - isInitialized: rootStore.toolStore.isInitialized - }, - important: true, - }) - - // Initialize tools if not initialized - if (!rootStore.toolStore.isInitialized) { - log({ - name: "[ChatActions] Tools not initialized", - preview: "Initializing tools", - important: true, - }) - yield rootStore.toolStore.initializeDefaultTools() - } - - const tool = rootStore.toolStore.getToolById(toolId) - if (!tool) { - throw new Error(`Tool ${functionCall.name} not found`) - } - - log({ - name: "[ChatActions] Found tool", - preview: "Tool details", - value: { - toolId: tool.id, - name: tool.name, - enabled: tool.enabled, - hasImplementation: !!tool.implementation - }, - important: true, - }) - - if (!tool.implementation) { - throw new Error(`Tool ${functionCall.name} implementation not found`) - } - - // Execute the tool - log({ - name: "[ChatActions] Executing tool", - preview: "Running implementation", - value: { args: functionCall.args }, - important: true, - }) - - const toolResult = yield tool.implementation(functionCall.args) - - log({ - name: "[ChatActions] Tool result", - preview: "Got result", - value: { toolResult }, - important: true, - }) - - if (!toolResult.success) { - throw new Error(toolResult.error) - } - - // Create function message using MessageModel - const functionMessage = MessageModel.create({ - id: Math.random().toString(36).substring(2, 9), - role: "assistant", - content: typeof toolResult.data === 'string' - ? toolResult.data - : JSON.stringify(toolResult.data, null, 2), - createdAt: Date.now(), - metadata: { - conversationId: self.currentConversationId, - name: functionCall.name, - isToolResult: true, - }, - }) - - self.messages.push(functionMessage) - - // Get analysis from the model using the full conversation context - const analysisResult = yield geminiChatApi.createChatCompletion( - [...self.currentMessages, functionMessage], - { - temperature: 0.7, - maxOutputTokens: 1024, - } - ) - - if (analysisResult.kind !== "ok") { - throw new Error("Failed to get analysis from model") - } - - // Update message with analysis - self.updateMessage(assistantMessage.id, { - content: analysisResult.response.choices[0].message.content, - metadata: { - ...assistantMessage.metadata, - isGenerating: false, - tokens: analysisResult.response.usage?.completion_tokens, - model: self.activeModel, - toolResult: toolResult.data, - }, - }) - return - } catch (toolError: unknown) { - // Log tool execution error - log.error("[ChatActions] Tool execution error:", toolError) - - // Update message with error - self.updateMessage(assistantMessage.id, { - content: `Error executing tool: ${toolError instanceof Error ? toolError.message : 'Unknown error'}`, - metadata: { - ...assistantMessage.metadata, - isGenerating: false, - error: toolError instanceof Error ? toolError.message : 'Unknown error', - model: self.activeModel, - }, - }) - return - } - } else { - // Regular message response - self.updateMessage(assistantMessage.id, { - content: content, - metadata: { - ...assistantMessage.metadata, - isGenerating: false, - tokens: response.usage?.completion_tokens, - model: self.activeModel, - }, - }) - } - } else { - // Handle error - self.updateMessage(assistantMessage.id, { - content: "Sorry, I encountered an error while processing your message: " + JSON.stringify(result), - metadata: { - ...assistantMessage.metadata, - isGenerating: false, - error: result.kind, - model: self.activeModel, - }, - }) - if (__DEV__) { - log.error("[ChatActions]", `Error sending message: ${result.kind}`) - } - } - } catch (error) { - if (__DEV__) { - log.error("[ChatActions]", `Error in sendMessage: ${error}`) - } - self.setError("Failed to send message") - } finally { - self.setIsGenerating(false) - } - }), -}) \ No newline at end of file diff --git a/app/models/chat/ChatStore.ts b/app/models/chat/ChatStore.ts index 4aa63999..bed8cd0b 100644 --- a/app/models/chat/ChatStore.ts +++ b/app/models/chat/ChatStore.ts @@ -1,11 +1,11 @@ import { - applySnapshot, flow, Instance, onSnapshot, SnapshotIn, SnapshotOut, types + flow, Instance, onSnapshot, SnapshotIn, SnapshotOut, types } from "mobx-state-tree" import { log } from "@/utils/log" import { withSetPropAction } from "../_helpers/withSetPropAction" -// Add Groq actions after ChatStore is defined to avoid circular dependency -import { withGroqActions } from "./ChatActions" -import { initializeDatabase, loadChat, saveChat, getAllChats } from "./ChatStorage" +import { + getAllChats, initializeDatabase, loadChat, saveChat +} from "./ChatStorage" // Message Types export const MessageModel = types @@ -84,6 +84,15 @@ export const ChatStoreModel = types } }) + const loadAllChats = flow(function* () { + try { + const allChats = yield getAllChats() + self.chats.replace(allChats) + } catch (e) { + log.error("Error loading all chats:", e) + } + }) + return { addMessage(message: { role: "system" | "user" | "assistant" | "function" @@ -103,7 +112,7 @@ export const ChatStoreModel = types // Update chats list after adding a message updateChatsList() - + return msg }, @@ -131,7 +140,7 @@ export const ChatStoreModel = types setCurrentConversationId: flow(function* (id: string) { self.currentConversationId = id yield loadMessagesFromStorage() - + // Ensure this chat exists in the chats list const chatExists = self.chats.some(chat => chat.id === id) if (!chatExists) { @@ -164,14 +173,7 @@ export const ChatStoreModel = types self.enabledTools.replace(tools) }, - loadAllChats: flow(function* () { - try { - const allChats = yield getAllChats() - self.chats.replace(allChats) - } catch (e) { - log.error("Error loading all chats:", e) - } - }), + loadAllChats, afterCreate: flow(function* () { try { @@ -182,7 +184,7 @@ export const ChatStoreModel = types yield loadMessagesFromStorage() // Load all chats - yield self.loadAllChats() + yield loadAllChats() // Set up persistence listener onSnapshot(self.messages, (snapshot) => { @@ -224,15 +226,8 @@ export interface ChatStore extends Instance { } export interface ChatStoreSnapshotOut extends SnapshotOut { } export interface ChatStoreSnapshotIn extends SnapshotIn { } -// Create a new model that includes the Groq actions -export const ChatStoreWithActions = types.compose( - "ChatStoreWithActions", - ChatStoreModel, - types.model({}) -).actions(self => withGroqActions(self as ChatStore)) - export const createChatStoreDefaultModel = () => - ChatStoreWithActions.create({ + ChatStoreModel.create({ isInitialized: false, error: null, messages: [], diff --git a/app/models/coder/CoderStore.ts b/app/models/coder/CoderStore.ts index d1f9dfaa..acbe2848 100644 --- a/app/models/coder/CoderStore.ts +++ b/app/models/coder/CoderStore.ts @@ -1,4 +1,4 @@ -import { Instance, SnapshotIn, SnapshotOut, cast, types } from "mobx-state-tree" +import { cast, Instance, SnapshotIn, SnapshotOut, types } from "mobx-state-tree" import { withSetPropAction } from "../_helpers/withSetPropAction" import { Repo } from "../types/repo" @@ -9,23 +9,124 @@ export const RepoModel = types.model("Repo", { branch: types.string, }) +// GitHub Token Model +export const GithubTokenModel = types.model("GithubToken", { + id: types.identifier, + name: types.string, + token: types.string, +}) + export const CoderStoreModel = types .model("CoderStore") .props({ - isInitialized: types.optional(types.boolean, false), error: types.maybeNull(types.string), + // Keep the old token field for backward compatibility githubToken: types.optional(types.string, ""), + githubTokens: types.array(GithubTokenModel), + activeTokenId: types.maybeNull(types.string), repos: types.array(RepoModel), activeRepoIndex: types.maybeNull(types.number), }) .actions(withSetPropAction) .actions((self) => ({ + afterCreate() { + // CRITICAL: If there's an old token, ALWAYS ensure it exists in the new system + if (self.githubToken) { + const legacyTokenExists = self.githubTokens.some(t => + t.name === "Legacy Token" && t.token === self.githubToken + ) + + if (!legacyTokenExists) { + const id = `token_${Date.now()}` + const newToken = GithubTokenModel.create({ + id, + name: "Legacy Token", + token: self.githubToken, + }) + self.githubTokens.push(newToken) + self.activeTokenId = id + } + } + }, + setError(error: string | null) { self.error = error }, + // Keep the old method for backward compatibility setGithubToken(token: string) { self.githubToken = token + // Also add/update in new system + if (token) { + const existingToken = self.githubTokens.find(t => t.name === "Legacy Token") + if (existingToken) { + this.updateGithubToken(existingToken.id, "Legacy Token", token) + } else { + this.addGithubToken("Legacy Token", token) + } + } + }, + + // Token actions + addGithubToken(name: string, token: string) { + const id = `token_${Date.now()}` + const newToken = GithubTokenModel.create({ id, name, token }) + self.githubTokens.push(newToken) + if (!self.activeTokenId) { + self.activeTokenId = id + self.githubToken = token // Keep legacy token in sync + } + }, + + removeGithubToken(id: string) { + const index = self.githubTokens.findIndex(t => t.id === id) + if (index !== -1) { + const token = self.githubTokens[index] + + // If removing active token, update legacy token + if (self.activeTokenId === id) { + // Find another token to make active + const remainingTokens = self.githubTokens.filter((_, i) => i !== index) + if (remainingTokens.length > 0) { + self.activeTokenId = remainingTokens[0].id + self.githubToken = remainingTokens[0].token + } else { + self.activeTokenId = null + // Clear legacy token if we're removing it + if (token.name === "Legacy Token") { + self.githubToken = "" + } + } + } + + self.githubTokens = cast(self.githubTokens.filter((_, i) => i !== index)) + } + }, + + updateGithubToken(id: string, name: string, token: string) { + const index = self.githubTokens.findIndex(t => t.id === id) + if (index !== -1) { + self.githubTokens = cast(self.githubTokens.map((t, i) => + i === index ? GithubTokenModel.create({ id, name, token }) : t + )) + // If updating active token, update legacy token + if (self.activeTokenId === id) { + self.githubToken = token + } + } + }, + + setActiveTokenId(id: string | null) { + if (id === null || self.githubTokens.some(t => t.id === id)) { + self.activeTokenId = id + // Update legacy token + if (id) { + const token = self.githubTokens.find(t => t.id === id) + if (token) { + self.githubToken = token.token + } + } + } }, // Repo actions @@ -43,7 +144,7 @@ export const CoderStoreModel = types repo.name === repoToRemove.name && repo.branch === repoToRemove.branch ) - + if (index !== -1) { self.repos = cast(self.repos.filter((_, i) => i !== index)) if (self.activeRepoIndex === index) { @@ -60,7 +161,7 @@ export const CoderStoreModel = types repo.name === oldRepo.name && repo.branch === oldRepo.branch ) - + if (index !== -1) { self.repos = cast(self.repos.map((repo, i) => i === index ? RepoModel.create(newRepo) : repo @@ -80,27 +181,39 @@ export const CoderStoreModel = types self.activeRepoIndex = null return } - + const index = self.repos.findIndex(r => r.owner === repo.owner && r.name === repo.name && r.branch === repo.branch ) - + if (index !== -1) { self.activeRepoIndex = index } } })) - .views((self) => ({ - get hasGithubToken() { - return !!self.githubToken - }, - - get activeRepo() { - return self.activeRepoIndex !== null ? self.repos[self.activeRepoIndex] : null + .views((self) => { + const views = { + get activeToken() { + return self.activeTokenId ? self.githubTokens.find(t => t.id === self.activeTokenId) : null + }, + + get githubTokenValue() { + // CRITICAL: Always return the legacy token if it exists + return self.githubToken || (views.activeToken ? views.activeToken.token : "") || "" + }, + + get hasGithubToken() { + return !!(self.githubToken || self.githubTokens.length > 0) + }, + + get activeRepo() { + return self.activeRepoIndex !== null ? self.repos[self.activeRepoIndex] : null + } } - })) + return views + }) export interface CoderStore extends Instance { } export interface CoderStoreSnapshotOut extends SnapshotOut { } @@ -108,9 +221,10 @@ export interface CoderStoreSnapshotIn extends SnapshotIn export const createCoderStoreDefaultModel = () => CoderStoreModel.create({ - isInitialized: false, error: null, githubToken: "", + githubTokens: [], + activeTokenId: null, repos: [], activeRepoIndex: null, }) \ No newline at end of file diff --git a/app/models/tools/ToolActions.ts b/app/models/tools/ToolActions.ts deleted file mode 100644 index eb873001..00000000 --- a/app/models/tools/ToolActions.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { flow, Instance } from "mobx-state-tree" -import { log } from "@/utils/log" -import type { ToolResult } from "../../services/gemini/tools/types" - -/** - * Tool actions for managing tool execution and state - */ -export const withToolActions = (self: Instance) => ({ - /** - * Executes a tool and handles the result - */ - executeTool: flow(function* ( - toolId: string, - params: Record - ): Generator, any> { - try { - const tool = self.getToolById(toolId) - if (!tool) { - throw new Error(`Tool ${toolId} not found`) - } - - if (!tool.enabled) { - throw new Error(`Tool ${toolId} is disabled`) - } - - // Mark tool as used - tool.markUsed() - - // Execute tool implementation - const implementation = tool.metadata.implementation - if (!implementation) { - throw new Error(`No implementation found for tool ${toolId}`) - } - - const result = yield implementation(params) - - // Update tool metadata with result - tool.updateMetadata({ - lastResult: result, - lastExecuted: Date.now(), - }) - - return { - success: true, - data: result - } - } catch (error) { - log.error("[ToolActions]", error instanceof Error ? error.message : "Unknown error") - return { - success: false, - error: error instanceof Error ? error.message : "Unknown error executing tool" - } - } - }), - - /** - * Initializes default tools - */ - initializeDefaultTools: flow(function* () { - try { - // Add GitHub tools - self.addTool({ - id: "github_view_file", - name: "view_file", - description: "View file contents at path", - parameters: { - path: "string", - owner: "string", - repo: "string", - branch: "string" - }, - metadata: { - category: "github", - implementation: async (params: Record) => { - const { viewFile } = await import("../../services/gemini/tools/github-impl") - return viewFile(params as any) - } - } - }) - - self.addTool({ - id: "github_view_hierarchy", - name: "view_hierarchy", - description: "View file/folder hierarchy at path", - parameters: { - path: "string", - owner: "string", - repo: "string", - branch: "string" - }, - metadata: { - category: "github", - implementation: async (params: Record) => { - const { viewHierarchy } = await import("../../services/gemini/tools/github-impl") - return viewHierarchy(params as any) - } - } - }) - - self.isInitialized = true - } catch (error) { - log.error("[ToolActions]", error instanceof Error ? error.message : "Unknown error") - self.setError("Failed to initialize tools") - } - }) -}) \ No newline at end of file diff --git a/app/models/tools/ToolStore.ts b/app/models/tools/ToolStore.ts deleted file mode 100644 index 9a915628..00000000 --- a/app/models/tools/ToolStore.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { - Instance, - SnapshotIn, - SnapshotOut, - types, - flow, -} from "mobx-state-tree" -import { withSetPropAction } from "../_helpers/withSetPropAction" -import { log } from "@/utils/log" -import { viewFile, viewHierarchy } from "../../services/gemini/tools/github-impl" -import { githubTools } from "../../services/gemini/tools/github" - -export const ToolModel = types - .model("Tool", { - id: types.identifier, - name: types.string, - description: types.string, - parameters: types.frozen(), - enabled: types.optional(types.boolean, true), - lastUsed: types.maybe(types.number), - metadata: types.optional(types.frozen(), {}), - }) - .volatile(self => ({ - implementation: undefined as ((...args: any[]) => Promise) | undefined, - })) - .actions(self => ({ - setEnabled(enabled: boolean) { - self.enabled = enabled - }, - updateMetadata(metadata: any) { - self.metadata = { ...self.metadata, ...metadata } - }, - markUsed() { - self.lastUsed = Date.now() - }, - setImplementation(implementation: (...args: any[]) => Promise) { - self.implementation = implementation - } - })) - -export interface ITool extends Instance { } - -export const ToolStoreModel = types - .model("ToolStore") - .props({ - tools: types.array(ToolModel), - isInitialized: types.optional(types.boolean, false), - error: types.maybeNull(types.string), - }) - .actions(withSetPropAction) - .actions((self) => { - // Define base actions first - const baseActions = { - setError(error: string | null) { - self.error = error - }, - - addTool(tool: { - id: string - name: string - description: string - parameters: Record - enabled?: boolean - metadata?: any - implementation?: (...args: any[]) => Promise - }) { - const existingTool = self.tools.find(t => t.id === tool.id) - if (existingTool) { - if (tool.implementation) { - existingTool.setImplementation(tool.implementation) - } - return existingTool - } - - const newTool = ToolModel.create({ - ...tool, - enabled: tool.enabled ?? true, - metadata: { - ...tool.metadata, - }, - }) - - if (tool.implementation) { - newTool.setImplementation(tool.implementation) - } - - self.tools.push(newTool) - return newTool - }, - - removeTool(id: string) { - const idx = self.tools.findIndex(t => t.id === id) - if (idx >= 0) { - self.tools.splice(idx, 1) - } - }, - - enableTool(id: string) { - const tool = self.tools.find(t => t.id === id) - if (tool) { - tool.setEnabled(true) - } - }, - - disableTool(id: string) { - const tool = self.tools.find(t => t.id === id) - if (tool) { - tool.setEnabled(false) - } - }, - } - - // Then define actions that depend on base actions - const flowActions = { - initializeDefaultTools: flow(function* () { - try { - log({ - name: "[ToolStore] initializeDefaultTools", - preview: "Initializing tools", - value: { currentTools: self.tools.map(t => t.id) }, - important: true, - }) - - // Add GitHub tools with implementations - const viewFileTool = baseActions.addTool({ - id: "github_view_file", - name: "view_file", - description: githubTools.viewFile.description, - parameters: githubTools.viewFile.parameters.properties, - metadata: { - category: "github", - }, - implementation: viewFile, - }) - - log({ - name: "[ToolStore] Tool added", - preview: "Added view_file tool", - value: { - id: viewFileTool.id, - hasImplementation: !!viewFileTool.implementation - }, - important: true, - }) - - const viewHierarchyTool = baseActions.addTool({ - id: "github_view_hierarchy", - name: "view_hierarchy", - description: githubTools.viewHierarchy.description, - parameters: githubTools.viewHierarchy.parameters.properties, - metadata: { - category: "github", - }, - implementation: viewHierarchy, - }) - - log({ - name: "[ToolStore] Tool added", - preview: "Added view_hierarchy tool", - value: { - id: viewHierarchyTool.id, - hasImplementation: !!viewHierarchyTool.implementation - }, - important: true, - }) - - self.isInitialized = true - - log({ - name: "[ToolStore] initializeDefaultTools", - preview: "Tools initialized", - value: { - tools: self.tools.map(t => ({ - id: t.id, - name: t.name, - hasImplementation: !!t.implementation - })) - }, - important: true, - }) - } catch (error) { - log.error("[ToolStore]", error instanceof Error ? error.message : "Unknown error") - baseActions.setError("Failed to initialize tools") - } - }) - } - - // Return all actions - return { - ...baseActions, - ...flowActions, - } - }) - .views((self) => ({ - get enabledTools() { - return self.tools.filter(tool => tool.enabled) - }, - - get githubTools() { - return self.tools.filter(tool => - tool.metadata.category === "github" && tool.enabled - ) - }, - - getToolById(id: string) { - const tool = self.tools.find(t => t.id === id) - log({ - name: "[ToolStore] getToolById", - preview: `Getting tool ${id}`, - value: { - id, - found: !!tool, - hasImplementation: tool ? !!tool.implementation : false, - tools: self.tools.map(t => ({ - id: t.id, - hasImplementation: !!t.implementation - })) - }, - important: true, - }) - return tool - } - })) - -export interface ToolStore extends Instance { } -export interface ToolStoreSnapshotOut extends SnapshotOut { } -export interface ToolStoreSnapshotIn extends SnapshotIn { } - -export const createToolStoreDefaultModel = () => - ToolStoreModel.create({ - tools: [], - isInitialized: false, - error: null, - }) \ No newline at end of file diff --git a/app/models/tools/index.ts b/app/models/tools/index.ts deleted file mode 100644 index 41aa8a08..00000000 --- a/app/models/tools/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ToolStore' \ No newline at end of file diff --git a/app/navigators/AppNavigator.tsx b/app/navigators/AppNavigator.tsx new file mode 100644 index 00000000..87152d41 --- /dev/null +++ b/app/navigators/AppNavigator.tsx @@ -0,0 +1,80 @@ +import { observer } from "mobx-react-lite" +import { ComponentProps } from "react" +import * as Screens from "@/screens" +import { useAppTheme, useThemeProvider } from "@/utils/useAppTheme" +import { NavigationContainer } from "@react-navigation/native" +import { createNativeStackNavigator, NativeStackScreenProps } from "@react-navigation/native-stack" +import Config from "../config" +import { navigationRef, useBackButtonHandler } from "./navigationUtilities" + +/** + * This type allows TypeScript to know what routes are defined in this navigator + * as well as what properties (if any) they might take when navigating to them. + * + * If no params are allowed, pass through `undefined`. Generally speaking, we + * recommend using your MobX-State-Tree store(s) to keep application state + * rather than passing state through navigation params. + * + * For more information, see this documentation: + * https://reactnavigation.org/docs/params/ + * https://reactnavigation.org/docs/typescript#type-checking-the-navigator + * https://reactnavigation.org/docs/typescript/#organizing-types + */ +export type AppStackParamList = { + Chat: undefined + Settings: undefined +} + +/** + * This is a list of all the route names that will exit the app if the back button + * is pressed while in that screen. Only affects Android. + */ +const exitRoutes = Config.exitRoutes + +export type AppStackScreenProps = NativeStackScreenProps< + AppStackParamList, + T +> + +// Documentation: https://reactnavigation.org/docs/stack-navigator/ +const Stack = createNativeStackNavigator() + +const AppStack = observer(function AppStack() { + const { + theme: { colors }, + } = useAppTheme() + + return ( + + + + + ) +}) + +export interface NavigationProps extends Partial> {} + +export const AppNavigator = observer(function AppNavigator(props: NavigationProps) { + const { themeScheme, navigationTheme, setThemeContextOverride, ThemeProvider } = + useThemeProvider("dark") + + useBackButtonHandler((routeName) => exitRoutes.includes(routeName)) + + return ( + + {/* @ts-ignore */} + + + + + ) +}) diff --git a/app/navigators/index.ts b/app/navigators/index.ts new file mode 100644 index 00000000..017e4211 --- /dev/null +++ b/app/navigators/index.ts @@ -0,0 +1,3 @@ +export * from "./AppNavigator" +export * from "./navigationUtilities" +// export other navigators from here diff --git a/app/navigators/navigationUtilities.ts b/app/navigators/navigationUtilities.ts new file mode 100644 index 00000000..6b0c2d51 --- /dev/null +++ b/app/navigators/navigationUtilities.ts @@ -0,0 +1,205 @@ +import { useEffect, useRef, useState } from "react" +import { BackHandler, Linking, Platform } from "react-native" +import { + createNavigationContainerRef, NavigationState, PartialState +} from "@react-navigation/native" +import Config from "../config" +import * as storage from "../utils/storage" +import { useIsMounted } from "../utils/useIsMounted" + +import type { PersistNavigationConfig } from "../config/config.base" +import type { AppStackParamList, NavigationProps } from "./AppNavigator" + +type Storage = typeof storage + +/** + * Reference to the root App Navigator. + * + * If needed, you can use this to access the navigation object outside of a + * `NavigationContainer` context. However, it's recommended to use the `useNavigation` hook whenever possible. + * @see [Navigating Without Navigation Prop]{@link https://reactnavigation.org/docs/navigating-without-navigation-prop/} + * + * The types on this reference will only let you reference top level navigators. If you have + * nested navigators, you'll need to use the `useNavigation` with the stack navigator's ParamList type. + */ +export const navigationRef = createNavigationContainerRef() + +/** + * Gets the current screen from any navigation state. + * @param {NavigationState | PartialState} state - The navigation state to traverse. + * @returns {string} - The name of the current screen. + */ +export function getActiveRouteName(state: NavigationState | PartialState): string { + const route = state.routes[state.index ?? 0] + + // Found the active route -- return the name + if (!route.state) return route.name as keyof AppStackParamList + + // Recursive call to deal with nested routers + return getActiveRouteName(route.state as NavigationState) +} + +const iosExit = () => false + +/** + * Hook that handles Android back button presses and forwards those on to + * the navigation or allows exiting the app. + * @see [BackHandler]{@link https://reactnative.dev/docs/backhandler} + * @param {(routeName: string) => boolean} canExit - Function that returns whether we can exit the app. + * @returns {void} + */ +export function useBackButtonHandler(canExit: (routeName: string) => boolean) { + // The reason we're using a ref here is because we need to be able + // to update the canExit function without re-setting up all the listeners + const canExitRef = useRef(Platform.OS !== "android" ? iosExit : canExit) + + useEffect(() => { + canExitRef.current = canExit + }, [canExit]) + + useEffect(() => { + // We'll fire this when the back button is pressed on Android. + const onBackPress = () => { + if (!navigationRef.isReady()) { + return false + } + + // grab the current route + const routeName = getActiveRouteName(navigationRef.getRootState()) + + // are we allowed to exit? + if (canExitRef.current(routeName)) { + // exit and let the system know we've handled the event + BackHandler.exitApp() + return true + } + + // we can't exit, so let's turn this into a back action + if (navigationRef.canGoBack()) { + navigationRef.goBack() + return true + } + + return false + } + + // Subscribe when we come to life + BackHandler.addEventListener("hardwareBackPress", onBackPress) + + // Unsubscribe when we're done + return () => BackHandler.removeEventListener("hardwareBackPress", onBackPress) + }, []) +} + +/** + * This helper function will determine whether we should enable navigation persistence + * based on a config setting and the __DEV__ environment (dev or prod). + * @param {PersistNavigationConfig} persistNavigation - The config setting for navigation persistence. + * @returns {boolean} - Whether to restore navigation state by default. + */ +function navigationRestoredDefaultState(persistNavigation: PersistNavigationConfig) { + if (persistNavigation === "always") return false + if (persistNavigation === "dev" && __DEV__) return false + if (persistNavigation === "prod" && !__DEV__) return false + + // all other cases, disable restoration by returning true + return true +} + +/** + * Custom hook for persisting navigation state. + * @param {Storage} storage - The storage utility to use. + * @param {string} persistenceKey - The key to use for storing the navigation state. + * @returns {object} - The navigation state and persistence functions. + */ +export function useNavigationPersistence(storage: Storage, persistenceKey: string) { + const [initialNavigationState, setInitialNavigationState] = + useState() + const isMounted = useIsMounted() + + const initNavState = navigationRestoredDefaultState(Config.persistNavigation) + const [isRestored, setIsRestored] = useState(initNavState) + + const routeNameRef = useRef() + + const onNavigationStateChange = (state: NavigationState | undefined) => { + const previousRouteName = routeNameRef.current + if (state !== undefined) { + const currentRouteName = getActiveRouteName(state) + + if (previousRouteName !== currentRouteName) { + // track screens. + if (__DEV__) { + console.log(currentRouteName) + } + } + + // Save the current route name for later comparison + routeNameRef.current = currentRouteName as keyof AppStackParamList + + // Persist state to storage + storage.save(persistenceKey, state) + } + } + + const restoreState = async () => { + try { + const initialUrl = await Linking.getInitialURL() + + // Only restore the state if app has not started from a deep link + if (!initialUrl) { + const state = (await storage.load(persistenceKey)) as NavigationProps["initialState"] | null + if (state) setInitialNavigationState(state) + } + } finally { + if (isMounted()) setIsRestored(true) + } + } + + useEffect(() => { + if (!isRestored) restoreState() + // runs once on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return { onNavigationStateChange, restoreState, isRestored, initialNavigationState } +} + +/** + * use this to navigate without the navigation + * prop. If you have access to the navigation prop, do not use this. + * @see {@link https://reactnavigation.org/docs/navigating-without-navigation-prop/} + * @param {unknown} name - The name of the route to navigate to. + * @param {unknown} params - The params to pass to the route. + */ +export function navigate(name: unknown, params?: unknown) { + if (navigationRef.isReady()) { + // @ts-expect-error + navigationRef.navigate(name as never, params as never) + } +} + +/** + * This function is used to go back in a navigation stack, if it's possible to go back. + * If the navigation stack can't go back, nothing happens. + * The navigationRef variable is a React ref that references a navigation object. + * The navigationRef variable is set in the App component. + */ +export function goBack() { + if (navigationRef.isReady() && navigationRef.canGoBack()) { + navigationRef.goBack() + } +} + +/** + * resetRoot will reset the root navigation state to the given params. + * @param {Parameters[0]} state - The state to reset the root to. + * @returns {void} + */ +export function resetRoot( + state: Parameters[0] = { index: 0, routes: [] }, +) { + if (navigationRef.isReady()) { + navigationRef.resetRoot(state) + } +} diff --git a/app/onyx/BottomButtons.styles.ts b/app/onyx/BottomButtons.styles.ts deleted file mode 100644 index 4e2ecc06..00000000 --- a/app/onyx/BottomButtons.styles.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { StyleSheet } from "react-native" - -export const styles = StyleSheet.create({ - bottomButtons: { - position: "absolute", - bottom: 40, - left: 0, - right: 0, - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - gap: 20, - zIndex: 8, - }, - iconButton: { - width: 56, - height: 56, - }, - configureButton: { - width: 24, - height: 24, - position: "absolute", - left: 75, - bottom: 56, - zIndex: 10, - }, - trashButton: { - width: 24, - height: 24, - position: "absolute", - right: 20, - bottom: 56, - zIndex: 10, - }, - toolsButton: { - width: 24, - height: 24, - position: "absolute", - left: 50, - bottom: 56, - zIndex: 10, - }, - reposButton: { - width: 24, - height: 24, - position: "absolute", - left: 130, - bottom: 56, - zIndex: 10, - }, - copyButton: { - width: 24, - height: 24, - position: "absolute", - right: 70, - bottom: 56, - zIndex: 10, - }, -}) diff --git a/app/onyx/BottomButtons.tsx b/app/onyx/BottomButtons.tsx deleted file mode 100644 index 6c715611..00000000 --- a/app/onyx/BottomButtons.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from "react" -import { Image, TouchableOpacity, View } from "react-native" -import { useStores } from "@/models" -import { colors } from "@/theme" -import { Ionicons } from "@expo/vector-icons" -import Clipboard from "@react-native-clipboard/clipboard" -import { styles } from "./BottomButtons.styles" - -interface BottomButtonsProps { - onTextPress: () => void - onVoicePress: () => void - onConfigurePress: () => void - onReposPress: () => void - setMessages: (messages: any) => void -} - -export const BottomButtons = ({ - onTextPress, - onVoicePress, - onConfigurePress, - onReposPress, - setMessages, -}: BottomButtonsProps) => { - const { chatStore } = useStores() - - const handleClearChat = () => { - chatStore.clearMessages() - setMessages([]) - } - - const handleCopyConversation = () => { - const text = chatStore.conversationText - Clipboard.setString(text) - } - - return ( - <> - {/* Repos Button */} - - - - - {/* Configure Button */} - {/* - - */} - - {/* Copy Button */} - - - - - {/* Trash Button */} - - - - - {/* Bottom Buttons */} - - - - - - - - - - ) -} diff --git a/app/onyx/ChatOverlayPrev.tsx b/app/onyx/ChatOverlayPrev.tsx deleted file mode 100644 index 44f4595f..00000000 --- a/app/onyx/ChatOverlayPrev.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { observer } from "mobx-react-lite" -import React, { useEffect, useRef } from "react" -import { ScrollView, TouchableOpacity, View } from "react-native" -import { useStores } from "@/models" -import { IMessage } from "@/models/chat/ChatStore" -import { log } from "@/utils/log" -import Clipboard from "@react-native-clipboard/clipboard" -import { MessageContent } from "./markdown/MessageContent" -import { styles as baseStyles } from "./styles" - -export const ChatOverlay = observer(() => { - const { chatStore, toolStore } = useStores() - const scrollViewRef = useRef(null) - - useEffect(() => { - // Initialize tools if not already initialized - const initTools = async () => { - if (!toolStore.isInitialized) { - try { - await toolStore.initializeDefaultTools() - log({ - name: "[ChatOverlay] Tools initialized", - preview: "Tools ready", - value: { tools: toolStore.tools.map((t) => t.id) }, - important: true, - }) - } catch (err) { - log.error("[ChatOverlay] Failed to initialize tools:", err) - } - } - } - initTools() - }, [toolStore]) - - useEffect(() => { - // Scroll to bottom whenever messages change - scrollViewRef.current?.scrollToEnd({ animated: true }) - }, [chatStore.currentMessages]) - - const copyToClipboard = (content: string) => { - Clipboard.setString(content) - } - - return ( - - - {chatStore.currentMessages.map((message: IMessage) => ( - copyToClipboard(message.content)} - activeOpacity={0.7} - > - - {/* @ts-ignore */} - - - - ))} - - - ) -}) diff --git a/app/onyx/ConfigureModal.tsx b/app/onyx/ConfigureModal.tsx deleted file mode 100644 index 10855941..00000000 --- a/app/onyx/ConfigureModal.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { observer } from "mobx-react-lite" -import React from "react" -import { Modal, Pressable, StyleSheet, Text, TouchableOpacity, View } from "react-native" -import { useStores } from "../models/_helpers/useStores" -import { colors, typography } from "../theme" -import { styles as baseStyles } from "./styles" - -interface ConfigureModalProps { - visible: boolean - onClose: () => void -} - -export const ConfigureModal = observer(({ visible, onClose }: ConfigureModalProps) => { - const { chatStore } = useStores() - - const handleModelChange = (model: "groq" | "gemini") => { - chatStore.setActiveModel(model) - } - - const handleSave = () => { - onClose() - } - - return ( - - - - - Cancel - - - Save - - - - Configure - - - Active Model - - handleModelChange("groq")} - > - Groq - - - handleModelChange("gemini")} - > - Gemini - - - - - - ) -}) - -const styles = StyleSheet.create({ - container: { - padding: 20, - backgroundColor: "black", - }, - title: { - fontSize: 24, - fontWeight: "bold", - marginBottom: 20, - color: colors.text, - }, - text: { - fontFamily: typography.primary.normal, - }, - section: { - marginBottom: 20, - }, - sectionTitle: { - fontSize: 16, - fontWeight: "bold", - marginBottom: 10, - color: colors.text, - }, - buttonContainer: { - flexDirection: "row", - justifyContent: "space-around", - marginBottom: 20, - }, - button: { - backgroundColor: colors.palette.accent500, - padding: 10, - borderRadius: 5, - minWidth: 120, - alignItems: "center", - opacity: 0.5, - }, - buttonActive: { - opacity: 1, - }, - buttonText: { - color: colors.palette.neutral100, - fontWeight: "bold", - }, -}) \ No newline at end of file diff --git a/app/onyx/OnyxLayout.tsx b/app/onyx/OnyxLayout.tsx deleted file mode 100644 index ed6d877c..00000000 --- a/app/onyx/OnyxLayout.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { fetch as expoFetch } from "expo/fetch" -import { observer } from "mobx-react-lite" -import React, { useEffect, useRef, useState } from "react" -import { View } from "react-native" -import { Message, ToolInvocation, useChat } from "@ai-sdk/react" -import Config from "../config" -import { useStores } from "../models/_helpers/useStores" -import { BottomButtons } from "./BottomButtons" -import { ChatOverlay } from "./ChatOverlay" -import { ConfigureModal } from "./ConfigureModal" -import { RepoSection } from "./repo/RepoSection" -import { TextInputModal } from "./TextInputModal" -import { VoiceInputModal } from "./VoiceInputModal" - -// Available tools for the AI -const availableTools = ["view_file", "view_folder"] - -export const OnyxLayout = observer(() => { - const { chatStore, coderStore } = useStores() - const [showTextInput, setShowTextInput] = useState(false) - const [showVoiceInput, setShowVoiceInput] = useState(false) - const [showConfigure, setShowConfigure] = useState(false) - const [showRepos, setShowRepos] = useState(false) - const [transcript, setTranscript] = useState("") - const pendingToolInvocations = useRef([]) - const [localMessages, setLocalMessages] = useState([]) - - const { isLoading, messages: aiMessages, error, append, setMessages } = useChat({ - fetch: expoFetch as unknown as typeof globalThis.fetch, - api: Config.NEXUS_URL, - body: { - // Only include GitHub token and tools if we have a token and at least one tool enabled - ...(coderStore.githubToken && - chatStore.enabledTools.length > 0 && { - githubToken: coderStore.githubToken, - tools: chatStore.enabledTools, - repos: coderStore.repos.map((repo) => ({ - owner: repo.owner, - name: repo.name, - branch: repo.branch, - })), - }), - }, - onError: (error) => { - console.error(error, "ERROR") - chatStore.setError(error.message || "An error occurred") - }, - onToolCall: async (toolCall) => { - console.log("TOOL CALL", toolCall) - }, - onFinish: (message, options) => { - console.log("FINISH", { message, options }) - chatStore.setIsGenerating(false) - - // Add assistant message to store - if (message.role === "assistant") { - chatStore.addMessage({ - role: "assistant", - content: message.content, - metadata: { - conversationId: chatStore.currentConversationId, - usage: options.usage, - finishReason: options.finishReason, - toolInvocations: pendingToolInvocations.current, - }, - }) - // Clear pending tool invocations - pendingToolInvocations.current = [] - } - }, - }) - - // Watch messages for tool invocations - useEffect(() => { - const lastMessage = aiMessages[aiMessages.length - 1] - if (lastMessage?.role === "assistant" && lastMessage.toolInvocations?.length > 0) { - console.log("Found tool invocations:", lastMessage.toolInvocations) - pendingToolInvocations.current = lastMessage.toolInvocations - } - }, [aiMessages]) - - // Sync store messages with local state - useEffect(() => { - const storedMessages = chatStore.currentMessages - const chatMessages: Message[] = storedMessages.map((msg) => ({ - id: msg.id, - role: msg.role as "user" | "assistant" | "system", - content: msg.content, - createdAt: new Date(msg.createdAt), - // Restore tool invocations if they exist - ...(msg.metadata?.toolInvocations - ? { - toolInvocations: msg.metadata.toolInvocations, - } - : {}), - })) - setLocalMessages(chatMessages) - }, [chatStore.currentMessages]) - - // Load persisted messages when conversation changes - useEffect(() => { - const loadMessages = async () => { - // First clear the useChat messages - setMessages([]) - - // Then load the persisted messages from store - const storedMessages = chatStore.currentMessages - if (storedMessages.length > 0) { - // Convert store messages to useChat format - const chatMessages: Message[] = storedMessages.map((msg) => ({ - id: msg.id, - role: msg.role as "user" | "assistant" | "system", - content: msg.content, - createdAt: new Date(msg.createdAt), - // Restore tool invocations if they exist - ...(msg.metadata?.toolInvocations - ? { - toolInvocations: msg.metadata.toolInvocations, - } - : {}), - })) - setMessages(chatMessages) - } - } - - loadMessages() - }, [chatStore.currentConversationId]) - - const handleStartVoiceInput = () => { - setTranscript("") // Reset transcript - setShowVoiceInput(true) - // TODO: Start voice recording here - } - - const handleStopVoiceInput = () => { - // TODO: Stop voice recording here - setShowVoiceInput(false) - setTranscript("") - } - - const handleSendMessage = async (message: string) => { - // Reset pending tool invocations for new message - pendingToolInvocations.current = [] - - // Add user message to store first - chatStore.addMessage({ - role: "user", - content: message, - metadata: { - conversationId: chatStore.currentConversationId, - }, - }) - - // Set generating state - chatStore.setIsGenerating(true) - - // Send to AI - await append({ - content: message, - role: "user", - createdAt: new Date(), - }) - } - - return ( - - - - setShowTextInput(false)} - onSendMessage={handleSendMessage} - /> - - - - setShowConfigure(false)} /> - - setShowRepos(false)} /> - - setShowTextInput(true)} - onVoicePress={handleStartVoiceInput} - onConfigurePress={() => setShowConfigure(true)} - onReposPress={() => setShowRepos(true)} - setMessages={setMessages} - /> - - ) -}) \ No newline at end of file diff --git a/app/onyx/TextInputModal.tsx b/app/onyx/TextInputModal.tsx deleted file mode 100644 index 7032d0b7..00000000 --- a/app/onyx/TextInputModal.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React, { useState } from "react" -import { Modal, TextInput, View, Text, Pressable } from "react-native" -import { styles as baseStyles } from "./styles" -import { observer } from "mobx-react-lite" -import { log } from "@/utils/log" - -interface TextInputModalProps { - visible: boolean - onClose: () => void - onSendMessage: (message: string) => Promise -} - -export const TextInputModal = observer(({ visible, onClose, onSendMessage }: TextInputModalProps) => { - const [text, setText] = useState("") - const [isGenerating, setIsGenerating] = useState(false) - - const handleSend = async () => { - if (!text.trim()) return - - try { - const messageToSend = text // Capture current text - setText("") // Clear input - onClose() // Close modal immediately - setIsGenerating(true) - - // Send message after modal is closed - await onSendMessage(messageToSend) - } catch (error) { - log({ - name: "[TextInputModal]", - preview: "Error sending message", - value: error instanceof Error ? error.message : "Unknown error", - important: true - }) - } finally { - setIsGenerating(false) - } - } - - const isDisabled = !text.trim() || isGenerating - - return ( - - - - - Cancel - - - - - Send - - - - - - - - ) -}) \ No newline at end of file diff --git a/app/onyx/VoiceInputModal.styles.ts b/app/onyx/VoiceInputModal.styles.ts deleted file mode 100644 index 4ba28b25..00000000 --- a/app/onyx/VoiceInputModal.styles.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { StyleSheet } from "react-native" -import { typography } from "@/theme/typography" - -export const styles = StyleSheet.create({ - voiceContainer: { - flex: 1, - justifyContent: "center", - alignItems: "center", - padding: 20, - }, - transcriptionContainer: { - width: "100%", - alignItems: "center", - justifyContent: "center", - }, - listeningContainer: { - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - marginBottom: 10, - marginTop: -120, - height: 110 - }, - listeningText: { - color: "#FFFFFF", - fontSize: 22, - fontFamily: typography.primary.normal, - marginRight: 10, - }, - placeholderText: { - color: "#999999", - fontSize: 16, - fontFamily: typography.primary.normal, - textAlign: "center", - }, - errorText: { - color: "#FF4444", - fontSize: 16, - fontFamily: typography.primary.normal, - textAlign: "center", - }, -}) diff --git a/app/onyx/VoiceInputModal.tsx b/app/onyx/VoiceInputModal.tsx deleted file mode 100644 index 42494955..00000000 --- a/app/onyx/VoiceInputModal.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { Audio } from "expo-av" -import { observer } from "mobx-react-lite" -import React, { useEffect, useRef, useState } from "react" -import { ActivityIndicator, Modal, Pressable, Text, View } from "react-native" -import { groqChatApi } from "@/services/groq/groq-chat" -import { log } from "@/utils/log" -import { styles as baseStyles } from "./styles" -import { styles as voiceStyles } from "./VoiceInputModal.styles" - -interface VoiceInputModalProps { - visible: boolean - onClose: () => void - transcript?: string - onSendMessage: (message: string) => Promise -} - -export const VoiceInputModal = observer( - ({ visible, onClose, transcript, onSendMessage }: VoiceInputModalProps) => { - const [isRecording, setIsRecording] = useState(false) - const [error, setError] = useState("") - const [isGenerating, setIsGenerating] = useState(false) - const recording = useRef(null) - - useEffect(() => { - return () => { - if (recording.current) { - recording.current.stopAndUnloadAsync().catch((err) => { - log.error( - "[VoiceInputModal] Error cleaning up: " + - (err instanceof Error ? err.message : String(err)), - ) - }) - } - } - }, []) - - useEffect(() => { - if (visible) { - setupRecording() - } else { - if (recording.current) { - recording.current.stopAndUnloadAsync().catch((err) => { - log.error( - "[VoiceInputModal] Error cleaning up: " + - (err instanceof Error ? err.message : String(err)), - ) - }) - recording.current = null - } - } - }, [visible]) - - const setupRecording = async () => { - try { - const { granted } = await Audio.requestPermissionsAsync() - if (!granted) { - setError("Microphone permission is required") - return - } - - await Audio.setAudioModeAsync({ - allowsRecordingIOS: true, - playsInSilentModeIOS: true, - }) - startRecording() - } catch (err) { - setError("Failed to get recording permissions") - log.error( - "[VoiceInputModal] Error setting up recording: " + - (err instanceof Error ? err.message : String(err)), - ) - } - } - - const startRecording = async () => { - try { - setError("") - const { recording: newRecording } = await Audio.Recording.createAsync( - Audio.RecordingOptionsPresets.HIGH_QUALITY, - ) - recording.current = newRecording - setIsRecording(true) - } catch (err) { - setError("Failed to start recording") - log.error( - "[VoiceInputModal] Error starting recording: " + - (err instanceof Error ? err.message : String(err)), - ) - } - } - - const handleCancel = () => { - onClose() - } - - const handleSend = async () => { - if (!recording.current) return - - const currentRecording = recording.current - recording.current = null - - // Close modal first - onClose() - - try { - setIsGenerating(true) - // Then stop recording and process - await currentRecording.stopAndUnloadAsync() - const uri = currentRecording.getURI() - - // Start transcription and chat process - if (uri) { - const result = await groqChatApi.transcribeAudio(uri, { - model: "whisper-large-v3", - language: "en", - }) - - if (result.kind === "ok") { - await onSendMessage(result.response.text) - } else { - log.error("[VoiceInputModal] Transcription error: " + JSON.stringify(result)) - } - } - } catch (err) { - log.error( - "[VoiceInputModal] Error in send process: " + - (err instanceof Error ? err.message : String(err)), - ) - } finally { - setIsGenerating(false) - } - } - - const isDisabled = isGenerating - - return ( - - - - - Cancel - - - - - Send - - - - - - {error ? ( - {error} - ) : ( - - - Listening - - - Speak, then press send - - )} - - - - ) - }, -) \ No newline at end of file diff --git a/app/onyx/markdown/MessageContentPrev.tsx b/app/onyx/markdown/MessageContentPrev.tsx deleted file mode 100644 index 0b65e8e8..00000000 --- a/app/onyx/markdown/MessageContentPrev.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from "react" -import { Linking, StyleSheet, View } from "react-native" -import Markdown from "react-native-markdown-display" -import { IMessage } from "@/models/chat/ChatStore" -import { colors } from "@/theme" -import { markdownStyles } from "./styles" - -interface MessageContentProps { - message: IMessage -} - -export function MessageContent({ message }: MessageContentProps) { - const handleLinkPress = (url: string) => { - // Handle link clicks - Linking.openURL(url).catch((err) => console.error("Failed to open URL:", err)) - // Return true to indicate we've handled the link - return true - } - - const isUserMessage = message.role === "user" - - return ( - - - {message.content} - - - ) -} - -const styles = StyleSheet.create({ - container: { - width: "100%", - }, - userMessage: { - paddingLeft: 12, - borderLeftWidth: 2, - borderLeftColor: colors.text, - opacity: 0.8, - }, -}) diff --git a/app/onyx/repo/RepoSection.tsx b/app/onyx/repo/RepoSection.tsx deleted file mode 100644 index fbb94a76..00000000 --- a/app/onyx/repo/RepoSection.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import React, { useState } from "react" -import { Modal, Text, TextInput, TouchableOpacity, View, Pressable, ScrollView, KeyboardAvoidingView, Platform } from "react-native" -import { observer } from "mobx-react-lite" -import { Ionicons } from "@expo/vector-icons" -import { useStores } from "../../models/_helpers/useStores" -import { colors, typography } from "../../theme" -import { styles as baseStyles } from "../styles" -import { Repo } from "../../models/types/repo" -import { RepoSectionProps, AVAILABLE_TOOLS } from "./types" -import { styles } from "./styles" - -export const RepoSection = observer(({ visible, onClose }: RepoSectionProps) => { - const { coderStore, chatStore } = useStores() - const [editingRepo, setEditingRepo] = useState(null) - const [githubToken, setGithubToken] = useState(coderStore.githubToken) - const [repoInput, setRepoInput] = useState({ - owner: "", - name: "", - branch: "" - }) - const [showRepoForm, setShowRepoForm] = useState(false) - - const handleRepoInputChange = (field: keyof Repo, value: string) => { - setRepoInput(prev => ({ ...prev, [field]: value })) - } - - const handleRepoSubmit = () => { - if (!repoInput.owner || !repoInput.name || !repoInput.branch) { - return // Don't submit if fields are empty - } - - if (editingRepo) { - coderStore.updateRepo(editingRepo, repoInput) - setEditingRepo(null) - } else { - coderStore.addRepo(repoInput) - } - setRepoInput({ owner: "", name: "", branch: "" }) - setShowRepoForm(false) - } - - const handleGithubTokenSubmit = () => { - coderStore.setGithubToken(githubToken) - } - - const handleAddRepoClick = () => { - setEditingRepo(null) - setRepoInput({ owner: "", name: "", branch: "" }) - setShowRepoForm(true) - } - - const handleEditRepo = (repo: Repo) => { - // Create a plain object copy of the repo data - const repoData = { - owner: repo.owner, - name: repo.name, - branch: repo.branch - } - setEditingRepo(repoData) - setRepoInput(repoData) - setShowRepoForm(true) - coderStore.setActiveRepo(repo) - } - - const handleRemoveRepo = (repo: Repo) => { - if (editingRepo && - editingRepo.owner === repo.owner && - editingRepo.name === repo.name && - editingRepo.branch === repo.branch - ) { - setEditingRepo(null) - setRepoInput({ owner: "", name: "", branch: "" }) - setShowRepoForm(false) - } - coderStore.removeRepo(repo) - } - - const handleToolToggle = (toolId: string) => { - chatStore.toggleTool(toolId) - } - - return ( - - - {/* This view is needed to make sure the close button doesn't get covered by the keyboard */} - - - - - - - - Configure AutoCoder - - Onyx can analyze or edit codebases. Add a GitHub token and connect repos. - - - - GitHub Token - - - - - Available Tools - {AVAILABLE_TOOLS.map((tool) => ( - handleToolToggle(tool.id)} - > - - - {tool.name} - {tool.description} - - - {chatStore.isToolEnabled(tool.id) && ( - - )} - - - - ))} - - - - - Connected Repositories - - Add Repo - - - - {showRepoForm && ( - - - {editingRepo ? "Edit Repository" : "Add Repository"} - - handleRepoInputChange("owner", value)} - placeholder="Owner" - placeholderTextColor={colors.palette.neutral400} - autoCapitalize="none" - autoCorrect={false} - spellCheck={false} - /> - handleRepoInputChange("name", value)} - placeholder="Repository name" - placeholderTextColor={colors.palette.neutral400} - autoCapitalize="none" - autoCorrect={false} - spellCheck={false} - /> - handleRepoInputChange("branch", value)} - placeholder="Branch" - placeholderTextColor={colors.palette.neutral400} - autoCapitalize="none" - autoCorrect={false} - spellCheck={false} - /> - - - - {editingRepo ? "Update Repository" : "Add Repository"} - - - { - setEditingRepo(null) - setRepoInput({ owner: "", name: "", branch: "" }) - setShowRepoForm(false) - }} - > - Cancel - - - - )} - - {coderStore.repos.map((repo) => ( - - handleEditRepo(repo)} - > - - {repo.owner}/{repo.name} - - - Branch: {repo.branch} - - - handleRemoveRepo(repo)} - > - Remove - - - ))} - - - - - ) -}) \ No newline at end of file diff --git a/app/screens/ChatScreen/ChatScreen.tsx b/app/screens/ChatScreen/ChatScreen.tsx new file mode 100644 index 00000000..59db024b --- /dev/null +++ b/app/screens/ChatScreen/ChatScreen.tsx @@ -0,0 +1,5 @@ +import { ChatDrawerContainer } from "@/chat/ChatDrawerContainer" + +export const ChatScreen = () => { + return +} diff --git a/app/screens/ChatScreen/index.ts b/app/screens/ChatScreen/index.ts new file mode 100644 index 00000000..28edf54e --- /dev/null +++ b/app/screens/ChatScreen/index.ts @@ -0,0 +1 @@ +export * from './ChatScreen' diff --git a/app/screens/SettingsScreen/SettingsScreen.tsx b/app/screens/SettingsScreen/SettingsScreen.tsx new file mode 100644 index 00000000..898e46ea --- /dev/null +++ b/app/screens/SettingsScreen/SettingsScreen.tsx @@ -0,0 +1,18 @@ +import { View } from "react-native" +import { useHeader } from "@/hooks/useHeader" +import { goBack } from "@/navigators" +import { colorsDark as colors } from "@/theme" +import { RepoSettings } from "./coder/RepoSettings" + +export const SettingsScreen = () => { + useHeader({ + title: "Settings", + leftIcon: "back", + onLeftPress: goBack, + }) + return ( + + {}} /> + + ) +} diff --git a/app/screens/SettingsScreen/coder/GithubTokenSection.tsx b/app/screens/SettingsScreen/coder/GithubTokenSection.tsx new file mode 100644 index 00000000..389bf7a9 --- /dev/null +++ b/app/screens/SettingsScreen/coder/GithubTokenSection.tsx @@ -0,0 +1,153 @@ +import { observer } from "mobx-react-lite" +import React, { useState } from "react" +import { Text, TextInput, TouchableOpacity, View } from "react-native" +import { useStores } from "@/models" +import { colorsDark as colors } from "@/theme" +import { styles } from "./styles" + +export const GithubTokenSection = observer(() => { + const { coderStore } = useStores() + const [showTokenForm, setShowTokenForm] = useState(false) + const [editingTokenId, setEditingTokenId] = useState(null) + const [tokenInput, setTokenInput] = useState({ + name: "", + token: "", + }) + + const handleAddTokenClick = () => { + setEditingTokenId(null) + setTokenInput({ name: "", token: "" }) + setShowTokenForm(true) + } + + const handleEditToken = (id: string) => { + const token = coderStore.githubTokens.find((t) => t.id === id) + if (token) { + setEditingTokenId(id) + setTokenInput({ name: token.name, token: token.token }) + setShowTokenForm(true) + } + } + + const handleTokenSubmit = () => { + if (!tokenInput.name || !tokenInput.token) return + + if (editingTokenId) { + coderStore.updateGithubToken(editingTokenId, tokenInput.name, tokenInput.token) + } else { + coderStore.addGithubToken(tokenInput.name, tokenInput.token) + } + setTokenInput({ name: "", token: "" }) + setShowTokenForm(false) + setEditingTokenId(null) + } + + const handleCancelEdit = () => { + setTokenInput({ name: "", token: "" }) + setShowTokenForm(false) + setEditingTokenId(null) + } + + return ( + + + GitHub Tokens + + Add Token + + + + + You can add one or more GitHub tokens, but only one can be active at once. Onyx will act as + the user associated with the token. + + + {showTokenForm && ( + + + {editingTokenId ? "Edit Token" : "Add Token"} + + setTokenInput((prev) => ({ ...prev, name: value }))} + placeholder="Token name" + placeholderTextColor={colors.palette.neutral400} + autoCapitalize="none" + autoCorrect={false} + spellCheck={false} + /> + setTokenInput((prev) => ({ ...prev, token: value }))} + placeholder="GitHub token" + placeholderTextColor={colors.palette.neutral400} + secureTextEntry={true} + autoCapitalize="none" + autoCorrect={false} + spellCheck={false} + /> + + + + {editingTokenId ? "Update Token" : "Add Token"} + + + + Cancel + + + + )} + + {coderStore.githubTokens.map((token) => ( + + coderStore.setActiveTokenId(token.id)} + > + {token.name} + + {token.token.slice(0, 4)}...{token.token.slice(-4)} + + + + handleEditToken(token.id)} + > + Edit + + coderStore.removeGithubToken(token.id)} + > + Remove + + + + ))} + + ) +}) diff --git a/app/screens/SettingsScreen/coder/RepoFormSection.tsx b/app/screens/SettingsScreen/coder/RepoFormSection.tsx new file mode 100644 index 00000000..b50ba01a --- /dev/null +++ b/app/screens/SettingsScreen/coder/RepoFormSection.tsx @@ -0,0 +1,82 @@ +import React from "react" +import { Text, TextInput, TouchableOpacity, View } from "react-native" +import { useStores } from "@/models" +import { Repo } from "@/models/types/repo" +import { colorsDark as colors } from "@/theme" +import { styles } from "./styles" + +interface RepoFormSectionProps { + editingRepo: Repo | null + repoInput: { + owner: string + name: string + branch: string + } + onRepoInputChange: (field: keyof Repo, value: string) => void + onRepoSubmit: () => void + onCancel: () => void +} + +export const RepoFormSection = ({ + editingRepo, + repoInput, + onRepoInputChange, + onRepoSubmit, + onCancel, +}: RepoFormSectionProps) => { + return ( + + + {editingRepo ? "Edit Repository" : "Add Repository"} + + onRepoInputChange("owner", value)} + placeholder="Owner" + placeholderTextColor={colors.palette.neutral400} + autoCapitalize="none" + autoCorrect={false} + spellCheck={false} + /> + onRepoInputChange("name", value)} + placeholder="Repository name" + placeholderTextColor={colors.palette.neutral400} + autoCapitalize="none" + autoCorrect={false} + spellCheck={false} + /> + onRepoInputChange("branch", value)} + placeholder="Branch" + placeholderTextColor={colors.palette.neutral400} + autoCapitalize="none" + autoCorrect={false} + spellCheck={false} + /> + + + + {editingRepo ? "Update Repository" : "Add Repository"} + + + + Cancel + + + + ) +} \ No newline at end of file diff --git a/app/screens/SettingsScreen/coder/RepoListSection.tsx b/app/screens/SettingsScreen/coder/RepoListSection.tsx new file mode 100644 index 00000000..6b031692 --- /dev/null +++ b/app/screens/SettingsScreen/coder/RepoListSection.tsx @@ -0,0 +1,68 @@ +import { observer } from "mobx-react-lite" +import React from "react" +import { Text, TouchableOpacity, View } from "react-native" +import { useStores } from "@/models" +import { Repo } from "@/models/types/repo" +import { styles } from "./styles" + +interface RepoListSectionProps { + editingRepo: Repo | null + onEditRepo: (repo: Repo) => void + onRemoveRepo: (repo: Repo) => void + onAddRepoClick: () => void +} + +export const RepoListSection = observer(({ + editingRepo, + onEditRepo, + onRemoveRepo, + onAddRepoClick, +}: RepoListSectionProps) => { + const { coderStore } = useStores() + + return ( + + + Connected Repositories + + Add Repo + + + + {coderStore.repos.map((repo) => ( + + onEditRepo(repo)} + > + + {repo.owner}/{repo.name} + + Branch: {repo.branch} + + onRemoveRepo(repo)} + > + Remove + + + ))} + + ) +}) \ No newline at end of file diff --git a/app/screens/SettingsScreen/coder/RepoSettings.tsx b/app/screens/SettingsScreen/coder/RepoSettings.tsx new file mode 100644 index 00000000..19ce19a1 --- /dev/null +++ b/app/screens/SettingsScreen/coder/RepoSettings.tsx @@ -0,0 +1,115 @@ +import { observer } from "mobx-react-lite" +import React, { useState } from "react" +import { KeyboardAvoidingView, Platform, ScrollView, Text } from "react-native" +import { useStores } from "@/models" +import { Repo } from "@/models/types/repo" +import { styles as baseStyles } from "@/theme/onyx" +import { GithubTokenSection } from "./GithubTokenSection" +import { RepoFormSection } from "./RepoFormSection" +import { RepoListSection } from "./RepoListSection" +import { styles } from "./styles" +import { ToolsSection } from "./ToolsSection" +import { RepoSettingsProps } from "./types" + +export const RepoSettings = observer(({ visible, onClose }: RepoSettingsProps) => { + const { coderStore } = useStores() + const [editingRepo, setEditingRepo] = useState(null) + const [repoInput, setRepoInput] = useState({ + owner: "", + name: "", + branch: "", + }) + const [showRepoForm, setShowRepoForm] = useState(false) + + const handleRepoInputChange = (field: keyof Repo, value: string) => { + setRepoInput((prev) => ({ ...prev, [field]: value })) + } + + const handleRepoSubmit = () => { + if (!repoInput.owner || !repoInput.name || !repoInput.branch) { + return // Don't submit if fields are empty + } + + if (editingRepo) { + coderStore.updateRepo(editingRepo, repoInput) + setEditingRepo(null) + } else { + coderStore.addRepo(repoInput) + } + setRepoInput({ owner: "", name: "", branch: "" }) + setShowRepoForm(false) + } + + const handleAddRepoClick = () => { + setEditingRepo(null) + setRepoInput({ owner: "", name: "", branch: "" }) + setShowRepoForm(true) + } + + const handleEditRepo = (repo: Repo) => { + const repoData = { + owner: repo.owner, + name: repo.name, + branch: repo.branch, + } + setEditingRepo(repoData) + setRepoInput(repoData) + setShowRepoForm(true) + coderStore.setActiveRepo(repo) + } + + const handleRemoveRepo = (repo: Repo) => { + if ( + editingRepo && + editingRepo.owner === repo.owner && + editingRepo.name === repo.name && + editingRepo.branch === repo.branch + ) { + setEditingRepo(null) + setRepoInput({ owner: "", name: "", branch: "" }) + setShowRepoForm(false) + } + coderStore.removeRepo(repo) + } + + const handleCancelEdit = () => { + setEditingRepo(null) + setRepoInput({ owner: "", name: "", branch: "" }) + setShowRepoForm(false) + } + + return ( + + + AutoCoder + + Onyx can analyze or edit codebases. Add a GitHub token and connect repos. + + + + + + + {showRepoForm && ( + + )} + + + + + ) +}) diff --git a/app/screens/SettingsScreen/coder/ToolsSection.tsx b/app/screens/SettingsScreen/coder/ToolsSection.tsx new file mode 100644 index 00000000..546f81a8 --- /dev/null +++ b/app/screens/SettingsScreen/coder/ToolsSection.tsx @@ -0,0 +1,45 @@ +import { observer } from "mobx-react-lite" +import React from "react" +import { Text, TouchableOpacity, View } from "react-native" +import { useStores } from "@/models" +import { styles } from "./styles" +import { AVAILABLE_TOOLS } from "./types" + +export const ToolsSection = observer(() => { + const { chatStore } = useStores() + + const handleToolToggle = (toolId: string) => { + chatStore.toggleTool(toolId) + } + + return ( + + Available Tools + {AVAILABLE_TOOLS.map((tool) => ( + handleToolToggle(tool.id)} + > + + + {tool.name} + {tool.description} + + + {chatStore.isToolEnabled(tool.id) && ( + + )} + + + + ))} + + ) +}) \ No newline at end of file diff --git a/app/onyx/repo/styles.ts b/app/screens/SettingsScreen/coder/styles.ts similarity index 90% rename from app/onyx/repo/styles.ts rename to app/screens/SettingsScreen/coder/styles.ts index 34550fdd..fbf50724 100644 --- a/app/onyx/repo/styles.ts +++ b/app/screens/SettingsScreen/coder/styles.ts @@ -1,5 +1,5 @@ import { StyleSheet } from "react-native" -import { colors, typography } from "../../theme" +import { colorsDark as colors, typography } from "@/theme" export const styles = StyleSheet.create({ container: { @@ -18,9 +18,10 @@ export const styles = StyleSheet.create({ flex: 1, }, title: { - fontSize: 24, + fontSize: 20, fontWeight: "bold", marginBottom: 8, + marginTop: 16, color: colors.palette.neutral800, // Light text }, subtitle: { @@ -64,7 +65,7 @@ export const styles = StyleSheet.create({ fontWeight: "bold", }, input: { - backgroundColor: colors.palette.neutral200, // Dark input background + backgroundColor: colors.backgroundSecondary, // Dark input background color: colors.palette.neutral800, // Light text padding: 10, borderRadius: 5, @@ -79,7 +80,7 @@ export const styles = StyleSheet.create({ gap: 10, }, button: { - backgroundColor: colors.palette.neutral200, // Dark button background + backgroundColor: colors.backgroundSecondary, // Dark button background padding: 10, borderRadius: 5, opacity: 0.5, @@ -120,12 +121,16 @@ export const styles = StyleSheet.create({ backgroundColor: colors.palette.angry500, minWidth: 80, }, + editButton: { + backgroundColor: colors.palette.neutral200, + minWidth: 80, + }, cancelEditButton: { backgroundColor: colors.palette.neutral300, flex: 1, }, toolButton: { - backgroundColor: colors.palette.neutral200, // Dark tool button background + backgroundColor: colors.backgroundSecondary, // Dark tool button background padding: 12, borderRadius: 5, marginBottom: 8, @@ -169,4 +174,4 @@ export const styles = StyleSheet.create({ fontSize: 16, fontWeight: "bold", }, -}) +}) \ No newline at end of file diff --git a/app/onyx/repo/types.ts b/app/screens/SettingsScreen/coder/types.ts similarity index 86% rename from app/onyx/repo/types.ts rename to app/screens/SettingsScreen/coder/types.ts index 942bf51f..6582e242 100644 --- a/app/onyx/repo/types.ts +++ b/app/screens/SettingsScreen/coder/types.ts @@ -1,6 +1,4 @@ -import { Repo } from "../../models/types/repo" - -export interface RepoSectionProps { +export interface RepoSettingsProps { visible: boolean onClose: () => void } @@ -16,4 +14,4 @@ export const AVAILABLE_TOOLS: Tool[] = [ { id: "view_folder", name: "View Folder", description: "View file/folder hierarchy at path" }, { id: "create_file", name: "Create File", description: "Create a new file at path with content" }, { id: "rewrite_file", name: "Rewrite File", description: "Rewrite file at path with new content" }, -] \ No newline at end of file +] diff --git a/app/screens/SettingsScreen/index.ts b/app/screens/SettingsScreen/index.ts new file mode 100644 index 00000000..2e54bc3c --- /dev/null +++ b/app/screens/SettingsScreen/index.ts @@ -0,0 +1 @@ +export * from './SettingsScreen' diff --git a/app/screens/index.ts b/app/screens/index.ts new file mode 100644 index 00000000..422e76dd --- /dev/null +++ b/app/screens/index.ts @@ -0,0 +1,2 @@ +export * from './ChatScreen' +export * from './SettingsScreen' diff --git a/app/services/gemini/gemini-api.types.ts b/app/services/gemini/gemini-api.types.ts deleted file mode 100644 index d5060056..00000000 --- a/app/services/gemini/gemini-api.types.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { ChatMessage as GroqChatMessage, ChatCompletionResponse } from "../groq/groq-api.types" - -export interface GeminiConfig { - apiKey: string - baseURL?: string - timeout?: number -} - -export interface GenerateContentConfig { - temperature?: number - maxOutputTokens?: number - topP?: number - topK?: number - tools?: any[] -} - -export interface FunctionDeclaration { - name: string - description: string - parameters: { - type: string - properties: Record - required: string[] - } -} - -export interface FunctionCall { - name: string - args: Record -} - -export interface ChatMessage { - role: "user" | "model" - content: string -} - -// Using same response format as Groq for consistency -export { ChatCompletionResponse } \ No newline at end of file diff --git a/app/services/gemini/gemini-chat.ts b/app/services/gemini/gemini-chat.ts deleted file mode 100644 index 20d88a6e..00000000 --- a/app/services/gemini/gemini-chat.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { ApiResponse, ApisauceInstance, create } from "apisauce" -import { log } from "@/utils/log" -import Config from "../../config" -import { MessageModel } from "../../models/chat/ChatStore" -import { ITool } from "../../models/tools/ToolStore" -import { GeneralApiProblem, getGeneralApiProblem } from "../api/apiProblem" -import { DEFAULT_SYSTEM_MESSAGE } from "../local-models/constants" - -import type { GeminiConfig, ChatMessage, ChatCompletionResponse, GenerateContentConfig, FunctionDeclaration } from "./gemini-api.types" -import type { IMessage } from "../../models/chat/ChatStore" - -const DEFAULT_CONFIG: GeminiConfig = { - apiKey: Config.GEMINI_API_KEY ?? "", - baseURL: "https://generativelanguage.googleapis.com/v1beta", - timeout: 30000, -} - -interface GeminiMessage { - role: "user" | "model"; - parts: Array<{ text: string }>; -} - -/** - * Manages chat interactions with the Gemini API. - */ -export class GeminiChatApi { - apisauce: ApisauceInstance - config: GeminiConfig - - constructor(config: GeminiConfig = DEFAULT_CONFIG) { - this.config = config - - this.apisauce = create({ - baseURL: this.config.baseURL, - timeout: this.config.timeout, - headers: { - "Content-Type": "application/json", - }, - }) - } - - /** - * Converts a tool to a Gemini function declaration - */ - private convertToolToFunctionDeclaration(tool: ITool): FunctionDeclaration { - // Convert parameters to Gemini format - const properties: Record = {} - for (const [key, value] of Object.entries(tool.parameters)) { - if (typeof value === 'object' && value !== null && 'type' in value && 'description' in value) { - properties[key] = { - type: String(value.type), - description: String(value.description), - } - } - } - - return { - name: tool.name, - description: tool.description, - parameters: { - type: "object", - properties, - required: Object.keys(tool.parameters) - } - } - } - - /** - * Converts ChatStore messages to Gemini API format - */ - private convertToGeminiMessages(messages: IMessage[]): GeminiMessage[] { - // For Gemini, convert system message to user message and prepend it - const systemMessage = messages.find(msg => msg.role === "system") - const nonSystemMessages = messages.filter(msg => msg.role !== "system") - - if (systemMessage) { - nonSystemMessages.unshift({ - ...systemMessage, - role: "user" - }) - } else { - // Add default system message as user message - nonSystemMessages.unshift(MessageModel.create({ - id: "system", - role: "user", - content: DEFAULT_SYSTEM_MESSAGE.content, - createdAt: Date.now(), - metadata: {}, - })) - } - - // Filter out any messages with empty content and ensure content is a string - return nonSystemMessages - .filter(msg => msg.content && msg.content.trim() !== "") - .map(msg => ({ - role: msg.role === "assistant" ? "model" : msg.role === "function" ? "model" : "user", - parts: [{ text: msg.content.trim() }] - })) - } - - /** - * Creates a chat completion with the Gemini API - */ - async createChatCompletion( - messages: IMessage[], - options: GenerateContentConfig = {}, - ): Promise<{ kind: "ok"; response: ChatCompletionResponse } | GeneralApiProblem> { - try { - const geminiMessages = this.convertToGeminiMessages(messages) - - // Validate that we have at least one message with non-empty content - if (geminiMessages.length === 0) { - throw new Error("No valid messages to send to Gemini API") - } - - // Convert tools to function declarations if provided - const functionDeclarations = options.tools?.map(tool => - this.convertToolToFunctionDeclaration(tool) - ) - - const payload = { - contents: geminiMessages, - generationConfig: { - temperature: options.temperature ?? 0.7, - maxOutputTokens: options.maxOutputTokens ?? 1024, - topP: options.topP ?? 0.8, - topK: options.topK ?? 10, - }, - tools: functionDeclarations ? [{ - functionDeclarations - }] : undefined, - safetySettings: [ - { - category: "HARM_CATEGORY_HARASSMENT", - threshold: "BLOCK_NONE" - }, - { - category: "HARM_CATEGORY_HATE_SPEECH", - threshold: "BLOCK_NONE" - }, - { - category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", - threshold: "BLOCK_NONE" - }, - { - category: "HARM_CATEGORY_DANGEROUS_CONTENT", - threshold: "BLOCK_NONE" - } - ] - } - - log({ - name: "[GeminiChatApi] createChatCompletion", - preview: "Request payload", - value: { payload }, - important: true, - }) - - const response: ApiResponse = await this.apisauce.post( - `/models/gemini-1.5-pro:generateContent?key=${this.config.apiKey}`, - payload - ) - - log({ - name: "[GeminiChatApi] createChatCompletion", - preview: "Chat completion response", - value: { response: response.data }, - important: true, - }) - - if (!response.ok) { - const problem = getGeneralApiProblem(response) - if (problem) return problem - } - - if (!response.data) throw new Error("No data received from Gemini API") - - // Handle function calls in response - const functionCall = response.data.candidates?.[0]?.content?.parts?.[0]?.functionCall - let content - if (functionCall) { - // Format function call to match expected structure - content = JSON.stringify({ - functionCall: { - name: functionCall.name, - args: functionCall.args - } - }) - } else { - content = response.data.candidates[0].content.parts[0].text - } - - // Convert Gemini response to Groq format for consistency - const formattedResponse: ChatCompletionResponse = { - id: "gemini-" + Date.now(), - object: "chat.completion", - created: Date.now(), - model: "gemini-1.5-pro", - choices: [{ - index: 0, - message: { - role: "assistant", - content, - }, - finish_reason: response.data.candidates[0].finishReason, - }], - usage: { - prompt_tokens: 0, // Gemini doesn't provide token counts - completion_tokens: 0, - total_tokens: 0, - }, - } - - return { kind: "ok", response: formattedResponse } - } catch (e) { - if (__DEV__) { - log.error("[GeminiChatApi] " + (e instanceof Error ? e.message : "Unknown error")) - } - return { kind: "bad-data" } - } - } -} - -// Singleton instance of the API for convenience -export const geminiChatApi = new GeminiChatApi() \ No newline at end of file diff --git a/app/services/gemini/index.ts b/app/services/gemini/index.ts deleted file mode 100644 index 086e8129..00000000 --- a/app/services/gemini/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./gemini-chat" -export * from "./gemini-api.types" \ No newline at end of file diff --git a/app/services/gemini/tools/github-impl.ts b/app/services/gemini/tools/github-impl.ts deleted file mode 100644 index 051ccd26..00000000 --- a/app/services/gemini/tools/github-impl.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { log } from "@/utils/log" -import type { FileToolResult, HierarchyToolResult, ToolResult } from "./types" - -const GITHUB_API_BASE = "https://api.github.com" - -/** - * Fetches file contents from GitHub - */ -export async function viewFile(params: { - path: string - owner: string - repo: string - branch: string -}): Promise> { - try { - const { path, owner, repo, branch } = params - - // Validate params - if (!path || !owner || !repo || !branch) { - throw new Error("Missing required parameters") - } - - // Fetch file content - const response = await fetch( - `${GITHUB_API_BASE}/repos/${owner}/${repo}/contents/${path}?ref=${branch}`, - { - headers: { - Accept: "application/vnd.github.v3+json", - "User-Agent": "Onyx-Agent", - }, - } - ) - - if (!response.ok) { - throw new Error(`GitHub API error: ${response.statusText}`) - } - - const data = await response.json() - - // GitHub returns base64 encoded content - const content = Buffer.from(data.content, "base64").toString() - - return { - success: true, - data: { - content, - path: data.path, - sha: data.sha, - size: data.size, - encoding: data.encoding, - }, - } - } catch (error) { - log.error("[GitHub Tools]", error instanceof Error ? error.message : "Unknown error") - return { - success: false, - error: error instanceof Error ? error.message : "Unknown error viewing file", - } - } -} - -/** - * Fetches directory structure from GitHub - */ -export async function viewHierarchy(params: { - path: string - owner: string - repo: string - branch: string -}): Promise> { - try { - const { path, owner, repo, branch } = params - - // Validate params - if (!owner || !repo || !branch) { - throw new Error("Missing required parameters") - } - - // Fetch directory contents - const response = await fetch( - `${GITHUB_API_BASE}/repos/${owner}/${repo}/contents/${path}?ref=${branch}`, - { - headers: { - Accept: "application/vnd.github.v3+json", - "User-Agent": "Onyx-Agent", - }, - } - ) - - if (!response.ok) { - throw new Error(`GitHub API error: ${response.statusText}`) - } - - const data = await response.json() - - // Convert GitHub response to our hierarchy format - const hierarchy: HierarchyToolResult[] = Array.isArray(data) - ? data.map(item => ({ - path: item.path, - type: item.type === "dir" ? "dir" : "file", - name: item.name, - })) - : [{ - path: data.path, - type: data.type === "dir" ? "dir" : "file", - name: data.name, - }] - - return { - success: true, - data: hierarchy, - } - } catch (error) { - log.error("[GitHub Tools]", error instanceof Error ? error.message : "Unknown error") - return { - success: false, - error: error instanceof Error ? error.message : "Unknown error viewing hierarchy", - } - } -} \ No newline at end of file diff --git a/app/services/gemini/tools/github.ts b/app/services/gemini/tools/github.ts deleted file mode 100644 index f5c260a9..00000000 --- a/app/services/gemini/tools/github.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { FunctionDeclaration } from "../gemini-api.types" -import { viewFile, viewHierarchy } from "./github-impl" - -export const githubTools: Record = { - viewFile: { - name: "view_file", - description: "View file contents at path", - parameters: { - type: "object", - properties: { - path: { - type: "string", - description: "The path of the file to view", - }, - owner: { - type: "string", - description: "The owner of the repository", - }, - repo: { - type: "string", - description: "The name of the repository", - }, - branch: { - type: "string", - description: "The branch to view the file from", - }, - }, - required: ["path", "owner", "repo", "branch"] - }, - }, - - viewHierarchy: { - name: "view_hierarchy", - description: "View file/folder hierarchy at path", - parameters: { - type: "object", - properties: { - path: { - type: "string", - description: "The path to view the hierarchy", - }, - owner: { - type: "string", - description: "The owner of the repository", - }, - repo: { - type: "string", - description: "The name of the repository", - }, - branch: { - type: "string", - description: "The branch to view the hierarchy from", - }, - }, - required: ["path", "owner", "repo", "branch"] - }, - }, -} \ No newline at end of file diff --git a/app/services/gemini/tools/index.ts b/app/services/gemini/tools/index.ts deleted file mode 100644 index 3fc40e21..00000000 --- a/app/services/gemini/tools/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './github' -export * from './types' \ No newline at end of file diff --git a/app/services/gemini/tools/types.ts b/app/services/gemini/tools/types.ts deleted file mode 100644 index 9bcc2bbb..00000000 --- a/app/services/gemini/tools/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -export interface ToolParameter { - type: string - description: string - enum?: string[] - required?: boolean -} - -export interface ToolDefinition { - name: string - description: string - parameters: Record - execute: (params: Record) => Promise -} - -export interface ToolResult { - success: boolean - data?: T - error?: string -} - -export interface FileToolResult { - content: string - path: string - sha?: string - size?: number - encoding?: string -} - -export interface HierarchyToolResult { - path: string - type: "file" | "dir" - name: string - children?: HierarchyToolResult[] -} \ No newline at end of file diff --git a/app/services/groq/groq-chat.ts b/app/services/groq/groq-chat.ts index 26026ca3..deab5559 100644 --- a/app/services/groq/groq-chat.ts +++ b/app/services/groq/groq-chat.ts @@ -3,7 +3,6 @@ import { log } from "@/utils/log" import Config from "../../config" import { MessageModel } from "../../models/chat/ChatStore" import { GeneralApiProblem, getGeneralApiProblem } from "../api/apiProblem" -import { DEFAULT_SYSTEM_MESSAGE } from "../local-models/constants" import type { GroqConfig, ChatMessage, ChatCompletionResponse, TranscriptionResponse, TranscriptionConfig } from "./groq-api.types" import type { IMessage } from "../../models/chat/ChatStore" @@ -33,75 +32,6 @@ export class GroqChatApi { }) } - /** - * Converts ChatStore messages to Groq API format - */ - private convertToGroqMessages(messages: IMessage[]): ChatMessage[] { - // Add system message if not present - if (!messages.find(msg => msg.role === "system")) { - const systemMessage = MessageModel.create({ - id: "system", - role: "system", - content: DEFAULT_SYSTEM_MESSAGE.content, - createdAt: Date.now(), - metadata: {}, - }) - messages = [systemMessage, ...messages] - } - return messages.map(msg => ({ - role: msg.role as "system" | "user" | "assistant", - content: msg.content - })) - } - - /** - * Creates a chat completion with the Groq API - */ - async createChatCompletion( - messages: IMessage[], - model: string = "llama-3.1-8b-instant", - options: { - temperature?: number - max_tokens?: number - top_p?: number - stop?: string | string[] - response_format?: { type: "json_object" } - } = {}, - ): Promise<{ kind: "ok"; response: ChatCompletionResponse } | GeneralApiProblem> { - try { - const groqMessages = this.convertToGroqMessages(messages) - - const response: ApiResponse = await this.apisauce.post( - "/chat/completions", - { - messages: groqMessages, - model, - ...options, - }, - ) - - log({ - name: "[GroqChatApi] createChatCompletion", - preview: "Chat completion response", - value: response.data, - important: true, - }) - - if (!response.ok) { - const problem = getGeneralApiProblem(response) - if (problem) return problem - } - - if (!response.data) throw new Error("No data received from Groq API") - - return { kind: "ok", response: response.data } - } catch (e) { - if (__DEV__) { - log.error("[GroqChatApi] " + (e instanceof Error ? e.message : "Unknown error")) - } - return { kind: "bad-data" } - } - } /** * Transcribes audio using the Groq API diff --git a/app/services/local-models/LocalModelService.ts b/app/services/local-models/LocalModelService.ts deleted file mode 100644 index f922d6bd..00000000 --- a/app/services/local-models/LocalModelService.ts +++ /dev/null @@ -1,158 +0,0 @@ -import * as FileSystem from "expo-file-system" -import { AppState } from "react-native" -import { AVAILABLE_MODELS } from "./constants" - -const MODELS_DIR = `${FileSystem.cacheDirectory}models` - -export interface ModelInfo { - key: string - displayName: string - path: string | null - status: "idle" | "downloading" | "initializing" | "ready" | "error" - progress: number - error?: string -} - -export class LocalModelService { - private downloadResumable: FileSystem.DownloadResumable | null = null - private currentTempPath: string | null = null - private onProgressCallback: ((progress: number) => void) | null = null - private isCancelled: boolean = false - - constructor() { - // Ensure models directory exists - FileSystem.makeDirectoryAsync(MODELS_DIR, { intermediates: true }).catch(console.error) - - // Handle app state changes - AppState.addEventListener("change", (nextAppState) => { - if (nextAppState === "background" && this.downloadResumable) { - this.cancelDownload() - } - }) - } - - async getLocalModels(): Promise { - const models: ModelInfo[] = [] - - for (const [key, model] of Object.entries(AVAILABLE_MODELS)) { - const path = `${MODELS_DIR}/${model.filename}` - const fileInfo = await FileSystem.getInfoAsync(path) - - models.push({ - key, - displayName: model.displayName, - path: fileInfo.exists ? path : null, - status: fileInfo.exists ? "ready" : "idle", - progress: 0 - }) - } - - return models - } - - async startDownload(modelKey: string, onProgress?: (progress: number) => void): Promise { - // Cancel any existing download first - await this.cancelDownload() - this.isCancelled = false - - const model = AVAILABLE_MODELS[modelKey] - if (!model) { - throw new Error("Invalid model selected") - } - - // Store temp path and callback so we can clean up if cancelled - this.currentTempPath = `${FileSystem.cacheDirectory}temp_${model.filename}` - this.onProgressCallback = onProgress || null - const finalPath = `${MODELS_DIR}/${model.filename}` - - // Create download - this.downloadResumable = FileSystem.createDownloadResumable( - `https://huggingface.co/${model.repoId}/resolve/main/${model.filename}`, - this.currentTempPath, - {}, - (downloadProgress) => { - if (!this.isCancelled) { - const progress = - (downloadProgress.totalBytesWritten / downloadProgress.totalBytesExpectedToWrite) * 100 - this.onProgressCallback?.(progress) - } - } - ) - - try { - // Start download - const result = await this.downloadResumable.downloadAsync() - if (this.isCancelled) { - return "" // Return empty string to indicate cancelled download - } - if (!result?.uri) { - throw new Error("Download failed - no URI received") - } - - // Validate file - const fileInfo = await FileSystem.getInfoAsync(result.uri) - if (!fileInfo.exists || fileInfo.size < 100 * 1024 * 1024) { - throw new Error("Downloaded file is invalid or too small") - } - - // Move to final location - await FileSystem.moveAsync({ - from: result.uri, - to: finalPath - }) - - this.downloadResumable = null - this.currentTempPath = null - this.onProgressCallback = null - return finalPath - - } catch (error) { - // If cancelled, don't treat as error - if (this.isCancelled) { - return "" - } - - // Clean up temp file - if (this.currentTempPath) { - try { - await FileSystem.deleteAsync(this.currentTempPath, { idempotent: true }) - } catch { } - this.currentTempPath = null - } - this.downloadResumable = null - this.onProgressCallback = null - - throw error - } - } - - async cancelDownload() { - if (this.downloadResumable) { - this.isCancelled = true - try { - // Cancel the download first - await this.downloadResumable.cancelAsync() - - // Clean up temp file after cancelling - if (this.currentTempPath) { - await FileSystem.deleteAsync(this.currentTempPath, { idempotent: true }) - this.currentTempPath = null - } - } catch (error) { - console.error("Error cancelling download:", error) - } - this.downloadResumable = null - this.onProgressCallback = null - } - } - - async deleteModel(modelKey: string): Promise { - const model = AVAILABLE_MODELS[modelKey] - if (!model) { - throw new Error("Invalid model selected") - } - - const path = `${MODELS_DIR}/${model.filename}` - await FileSystem.deleteAsync(path, { idempotent: true }) - } -} \ No newline at end of file diff --git a/app/services/local-models/constants.ts b/app/services/local-models/constants.ts deleted file mode 100644 index 6fab05b3..00000000 --- a/app/services/local-models/constants.ts +++ /dev/null @@ -1,36 +0,0 @@ -export interface ModelConfig { - repoId: string - filename: string - displayName: string -} - -export const AVAILABLE_MODELS: { [key: string]: ModelConfig } = { - '1B': { - repoId: 'hugging-quants/Llama-3.2-1B-Instruct-Q4_K_M-GGUF', - filename: 'llama-3.2-1b-instruct-q4_k_m.gguf', - displayName: 'Llama 3.2 1B' - }, - '3B': { - repoId: 'hugging-quants/Llama-3.2-3B-Instruct-Q4_K_M-GGUF', - filename: 'llama-3.2-3b-instruct-q4_k_m.gguf', - displayName: 'Llama 3.2 3B' - }, -} - -export const DEFAULT_MODEL_KEY = '1B' -export const DEFAULT_MODEL = AVAILABLE_MODELS[DEFAULT_MODEL_KEY] - -export const DEFAULT_SYSTEM_MESSAGE = { - role: 'system' as const, - content: `You are Onyx, the user's personal AI agent. Here is what the user knows about you: - -"Onyx is your personal AI agent that responds to voice commands, grows smarter & more capable over time, and earns you bitcoin. It's part of the OpenAgents network where every agent makes all agents smarter. - -You have tools to access GitHub APIs for view file and view folder hierarchy. Use them when the user asks.\\n\\n`, -} - -export const randId = () => Math.random().toString(36).substr(2, 9) -export const user = { id: 'y9d7f8pgn' } -export const systemId = 'h3o3lc5xj' -export const system = { id: systemId } -export const defaultConversationId = 'default' diff --git a/app/theme/colors.ts b/app/theme/colors.ts index 0e974905..52fd8548 100644 --- a/app/theme/colors.ts +++ b/app/theme/colors.ts @@ -1,51 +1,85 @@ const palette = { - neutral900: "#fafafa", // zinc-50 - neutral800: "#f4f4f5", // zinc-100 - neutral700: "#e4e4e7", // zinc-200 - neutral600: "#d4d4d8", // zinc-300 - neutral500: "#a1a1aa", // zinc-400 - neutral400: "#71717a", // zinc-500 - neutral300: "#52525b", // zinc-600 - neutral200: "#27272a", // zinc-800 - neutral100: "#18181b", // zinc-900 - neutral50: "#09090b", // zinc-950 + neutral100: "#FFFFFF", + neutral200: "#F4F2F1", + neutral300: "#D7CEC9", + neutral400: "#B6ACA6", + neutral500: "#978F8A", + neutral600: "#564E4A", + neutral700: "#3C3836", + neutral800: "#191015", + neutral900: "#000000", - primary600: "#f4f4f5", // zinc-100 - primary500: "#e4e4e7", // zinc-200 - primary400: "#d4d4d8", // zinc-300 - primary300: "#a1a1aa", // zinc-400 - primary200: "#71717a", // zinc-500 - primary100: "#52525b", // zinc-600 + primary100: "#F4E0D9", + primary200: "#E8C1B4", + primary300: "#DDA28E", + primary400: "#D28468", + primary500: "#C76542", + primary600: "#A54F31", - secondary500: "#f4f4f5", // zinc-100 - secondary400: "#e4e4e7", // zinc-200 - secondary300: "#d4d4d8", // zinc-300 - secondary200: "#a1a1aa", // zinc-400 - secondary100: "#71717a", // zinc-500 + secondary100: "#DCDDE9", + secondary200: "#BCC0D6", + secondary300: "#9196B9", + secondary400: "#626894", + secondary500: "#41476E", - accent500: "#f4f4f5", // zinc-100 - accent400: "#e4e4e7", // zinc-200 - accent300: "#d4d4d8", // zinc-300 - accent200: "#a1a1aa", // zinc-400 - accent100: "#71717a", // zinc-500 + accent100: "#FFEED4", + accent200: "#FFE1B2", + accent300: "#FDD495", + accent400: "#FBC878", + accent500: "#FFBB50", - angry100: "#e4e4e7", // zinc-200 - angry500: "#18181b", // zinc-900 + angry100: "#F2D6CD", + angry500: "#C03403", - overlay20: "rgba(39, 39, 42, 0.2)", // zinc-800 - overlay50: "rgba(39, 39, 42, 0.5)", // zinc-800 + overlay20: "rgba(25, 16, 21, 0.2)", + overlay50: "rgba(25, 16, 21, 0.5)", } as const export const colors = { + /** + * The palette is available to use, but prefer using the name. + * This is only included for rare, one-off cases. Try to use + * semantic names as much as possible. + */ palette, + /** + * A helper for making something see-thru. + */ transparent: "rgba(0, 0, 0, 0)", + /** + * The default text color in many components. + */ text: palette.neutral800, + /** + * Secondary text information. + */ textDim: palette.neutral600, - background: palette.neutral100, - border: palette.neutral200, + /** + * The default color of the screen background. + */ + background: palette.neutral200, + /** + * The default border color. + */ + border: palette.neutral400, + /** + * The main tinting color. + */ tint: palette.primary500, + /** + * The inactive tinting color. + */ tintInactive: palette.neutral300, + /** + * A subtle color used for lines. + */ separator: palette.neutral300, + /** + * Error messages. + */ error: palette.angry500, + /** + * Error Background. + */ errorBackground: palette.angry100, } as const diff --git a/app/theme/colorsDark.ts b/app/theme/colorsDark.ts new file mode 100644 index 00000000..02bf18c4 --- /dev/null +++ b/app/theme/colorsDark.ts @@ -0,0 +1,52 @@ +const palette = { + neutral900: "#fafafa", // zinc-50 + neutral800: "#f4f4f5", // zinc-100 + neutral700: "#e4e4e7", // zinc-200 + neutral600: "#d4d4d8", // zinc-300 + neutral500: "#a1a1aa", // zinc-400 + neutral400: "#71717a", // zinc-500 + neutral300: "#52525b", // zinc-600 + neutral200: "#27272a", // zinc-800 + neutral100: "#18181b", // zinc-900 + neutral50: "#09090b", // zinc-950 + + primary600: "#f4f4f5", // zinc-100 + primary500: "#e4e4e7", // zinc-200 + primary400: "#d4d4d8", // zinc-300 + primary300: "#a1a1aa", // zinc-400 + primary200: "#71717a", // zinc-500 + primary100: "#52525b", // zinc-600 + + secondary500: "#f4f4f5", // zinc-100 + secondary400: "#e4e4e7", // zinc-200 + secondary300: "#d4d4d8", // zinc-300 + secondary200: "#a1a1aa", // zinc-400 + secondary100: "#71717a", // zinc-500 + + accent500: "#f4f4f5", // zinc-100 + accent400: "#e4e4e7", // zinc-200 + accent300: "#d4d4d8", // zinc-300 + accent200: "#a1a1aa", // zinc-400 + accent100: "#71717a", // zinc-500 + + angry100: "#e4e4e7", // zinc-200 + angry500: "#18181b", // zinc-900 + + overlay20: "rgba(39, 39, 42, 0.2)", // zinc-800 + overlay50: "rgba(39, 39, 42, 0.5)", // zinc-800 +} as const + +export const colors = { + palette, + transparent: "rgba(0, 0, 0, 0)", + text: palette.neutral800, + textDim: palette.neutral600, + background: "#000", + backgroundSecondary: palette.neutral100, + border: palette.neutral200, + tint: palette.primary500, + tintInactive: palette.neutral300, + separator: palette.neutral300, + error: palette.angry500, + errorBackground: palette.angry100, +} as const diff --git a/app/theme/images.ts b/app/theme/images.ts new file mode 100644 index 00000000..e28b2cc2 --- /dev/null +++ b/app/theme/images.ts @@ -0,0 +1,3 @@ +export const images = { + thinking: require("../../assets/images/Thinking-Animation.gif"), +} diff --git a/app/theme/index.ts b/app/theme/index.ts index d9cb0fed..57d275a3 100644 --- a/app/theme/index.ts +++ b/app/theme/index.ts @@ -1,2 +1,80 @@ -export * from './colors' -export * from './typography' +import type { StyleProp } from "react-native" +import { colors as colorsLight } from "./colors" +import { colors as colorsDark } from "./colorsDark" +import { spacing as spacingLight } from "./spacing" +import { spacing as spacingDark } from "./spacingDark" +import { timing } from "./timing" +import { typography } from "./typography" + +// This supports "light" and "dark" themes by default. If undefined, it'll use the system theme +export type ThemeContexts = "light" | "dark" | undefined + +// Because we have two themes, we need to define the types for each of them. +// colorsLight and colorsDark should have the same keys, but different values. +export type Colors = typeof colorsLight | typeof colorsDark +// The spacing type needs to take into account the different spacing values for light and dark themes. +export type Spacing = typeof spacingLight | typeof spacingDark + +// These two are consistent across themes. +export type Timing = typeof timing +export type Typography = typeof typography + +// The overall Theme object should contain all of the data you need to style your app. +export interface Theme { + colors: Colors + spacing: Spacing + typography: Typography + timing: Timing + isDark: boolean +} + +// Here we define our themes. +export const lightTheme: Theme = { + colors: colorsLight, + spacing: spacingLight, + typography, + timing, + isDark: false, +} +export const darkTheme: Theme = { + colors: colorsDark, + spacing: spacingDark, + typography, + timing, + isDark: true, +} + +/** + * Represents a function that returns a styled component based on the provided theme. + * @template T The type of the style. + * @param theme The theme object. + * @returns The styled component. + * + * @example + * const $container: ThemedStyle = (theme) => ({ + * flex: 1, + * backgroundColor: theme.colors.background, + * justifyContent: "center", + * alignItems: "center", + * }) + * // Then use in a component like so: + * const Component = () => { + * const { themed } = useAppTheme() + * return + * } + */ +export type ThemedStyle = (theme: Theme) => T +export type ThemedStyleArray = ( + | ThemedStyle + | StyleProp + | (StyleProp | ThemedStyle)[] +)[] + +// Export the theme objects with backwards compatibility for the old theme structure. +export { colorsLight as colors } +export { colorsDark } +export { spacingLight as spacing } + +export * from "./styles" +export * from "./typography" +export * from "./timing" diff --git a/app/onyx/styles.ts b/app/theme/onyx.ts similarity index 100% rename from app/onyx/styles.ts rename to app/theme/onyx.ts diff --git a/app/theme/spacing.ts b/app/theme/spacing.ts new file mode 100644 index 00000000..b03e093e --- /dev/null +++ b/app/theme/spacing.ts @@ -0,0 +1,14 @@ +/** + Use these spacings for margins/paddings and other whitespace throughout your app. + */ +export const spacing = { + xxxs: 2, + xxs: 4, + xs: 8, + sm: 12, + md: 16, + lg: 24, + xl: 32, + xxl: 48, + xxxl: 64, +} as const diff --git a/app/theme/spacingDark.ts b/app/theme/spacingDark.ts new file mode 100644 index 00000000..a4239e4a --- /dev/null +++ b/app/theme/spacingDark.ts @@ -0,0 +1,14 @@ +const SPACING_MULTIPLIER = 1.0 + +// This is an example of how you can have different spacing values for different themes. +export const spacing = { + xxxs: 2 * SPACING_MULTIPLIER, + xxs: 4 * SPACING_MULTIPLIER, + xs: 8 * SPACING_MULTIPLIER, + sm: 12 * SPACING_MULTIPLIER, + md: 16 * SPACING_MULTIPLIER, + lg: 24 * SPACING_MULTIPLIER, + xl: 32 * SPACING_MULTIPLIER, + xxl: 48 * SPACING_MULTIPLIER, + xxxl: 64 * SPACING_MULTIPLIER, +} as const diff --git a/app/theme/styles.ts b/app/theme/styles.ts new file mode 100644 index 00000000..3c67cb6a --- /dev/null +++ b/app/theme/styles.ts @@ -0,0 +1,22 @@ +import { ViewStyle } from "react-native" +import { spacing } from "./spacing" + +/* Use this file to define styles that are used in multiple places in your app. */ +export const $styles = { + row: { flexDirection: "row" } as ViewStyle, + flex1: { flex: 1 } as ViewStyle, + flexWrap: { flexWrap: "wrap" } as ViewStyle, + + container: { + paddingTop: spacing.lg + spacing.xl, + paddingHorizontal: spacing.lg, + } as ViewStyle, + + toggleInner: { + width: "100%", + height: "100%", + alignItems: "center", + justifyContent: "center", + overflow: "hidden", + } as ViewStyle, +} diff --git a/app/theme/timing.ts b/app/theme/timing.ts new file mode 100644 index 00000000..b8b72038 --- /dev/null +++ b/app/theme/timing.ts @@ -0,0 +1,6 @@ +export const timing = { + /** + * The duration (ms) for quick animations. + */ + quick: 300, +} diff --git a/app/utils/polyfills.ts b/app/utils/polyfills.ts new file mode 100644 index 00000000..0c5a1470 --- /dev/null +++ b/app/utils/polyfills.ts @@ -0,0 +1,5 @@ +import "@/utils/crypto-polyfill" +import "text-encoding-polyfill" +import { Buffer } from "buffer" + +global.Buffer = Buffer diff --git a/app/utils/useAppTheme.ts b/app/utils/useAppTheme.ts new file mode 100644 index 00000000..73d03123 --- /dev/null +++ b/app/utils/useAppTheme.ts @@ -0,0 +1,118 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react" +import { StyleProp, useColorScheme } from "react-native" +import { DarkTheme, DefaultTheme, useTheme as useNavTheme } from "@react-navigation/native" +import { + type Theme, + type ThemeContexts, + type ThemedStyle, + type ThemedStyleArray, + lightTheme, + darkTheme, +} from "@/theme" +import * as SystemUI from "expo-system-ui" + +type ThemeContextType = { + themeScheme: ThemeContexts + setThemeContextOverride: (newTheme: ThemeContexts) => void +} + +// create a React context and provider for the current theme +export const ThemeContext = createContext({ + themeScheme: undefined, // default to the system theme + setThemeContextOverride: (_newTheme: ThemeContexts) => { + console.error("Tried to call setThemeContextOverride before the ThemeProvider was initialized") + }, +}) + +const themeContextToTheme = (themeContext: ThemeContexts): Theme => + themeContext === "dark" ? darkTheme : lightTheme + +const setImperativeTheming = (theme: Theme) => { + SystemUI.setBackgroundColorAsync(theme.colors.background) +} + +export const useThemeProvider = (initialTheme: ThemeContexts = undefined) => { + const colorScheme = useColorScheme() + const [overrideTheme, setTheme] = useState(initialTheme) + + const setThemeContextOverride = useCallback((newTheme: ThemeContexts) => { + setTheme(newTheme) + }, []) + + const themeScheme = overrideTheme || colorScheme || "light" + const navigationTheme = themeScheme === "dark" ? DarkTheme : DefaultTheme + + useEffect(() => { + setImperativeTheming(themeContextToTheme(themeScheme)) + }, [themeScheme]) + + return { + themeScheme, + navigationTheme, + setThemeContextOverride, + ThemeProvider: ThemeContext.Provider, + } +} + +interface UseAppThemeValue { + // The theme object from react-navigation + navTheme: typeof DefaultTheme + // A function to set the theme context override (for switching modes) + setThemeContextOverride: (newTheme: ThemeContexts) => void + // The current theme object + theme: Theme + // The current theme context "light" | "dark" + themeContext: ThemeContexts + // A function to apply the theme to a style object. + // See examples in the components directory or read the docs here: + // https://docs.infinite.red/ignite-cli/boilerplate/app/utils/ + themed: (styleOrStyleFn: ThemedStyle | StyleProp | ThemedStyleArray) => T +} + +/** + * Custom hook that provides the app theme and utility functions for theming. + * + * @returns {UseAppThemeReturn} An object containing various theming values and utilities. + * @throws {Error} If used outside of a ThemeProvider. + */ +export const useAppTheme = (): UseAppThemeValue => { + const navTheme = useNavTheme() + const context = useContext(ThemeContext) + if (!context) { + throw new Error("useTheme must be used within a ThemeProvider") + } + + const { themeScheme: overrideTheme, setThemeContextOverride } = context + + const themeContext: ThemeContexts = useMemo( + () => overrideTheme || (navTheme.dark ? "dark" : "light"), + [overrideTheme, navTheme], + ) + + const themeVariant: Theme = useMemo(() => themeContextToTheme(themeContext), [themeContext]) + + const themed = useCallback( + (styleOrStyleFn: ThemedStyle | StyleProp | ThemedStyleArray) => { + const flatStyles = [styleOrStyleFn].flat(3) + const stylesArray = flatStyles.map((f) => { + if (typeof f === "function") { + return (f as ThemedStyle)(themeVariant) + } else { + return f + } + }) + + // Flatten the array of styles into a single object + return Object.assign({}, ...stylesArray) as T + }, + [themeVariant], + ) + + return { + navTheme, + setThemeContextOverride, + theme: themeVariant, + themeContext, + themed, + } +} diff --git a/assets/icons/back.png b/assets/icons/back.png new file mode 100644 index 00000000..c74683be Binary files /dev/null and b/assets/icons/back.png differ diff --git a/assets/icons/back@2x.png b/assets/icons/back@2x.png new file mode 100644 index 00000000..3743cfbe Binary files /dev/null and b/assets/icons/back@2x.png differ diff --git a/assets/icons/back@3x.png b/assets/icons/back@3x.png new file mode 100644 index 00000000..203fe5b3 Binary files /dev/null and b/assets/icons/back@3x.png differ diff --git a/assets/icons/bell.png b/assets/icons/bell.png new file mode 100644 index 00000000..3928d7c1 Binary files /dev/null and b/assets/icons/bell.png differ diff --git a/assets/icons/bell@2x.png b/assets/icons/bell@2x.png new file mode 100644 index 00000000..53720f45 Binary files /dev/null and b/assets/icons/bell@2x.png differ diff --git a/assets/icons/bell@3x.png b/assets/icons/bell@3x.png new file mode 100644 index 00000000..58a77d02 Binary files /dev/null and b/assets/icons/bell@3x.png differ diff --git a/assets/icons/caretLeft.png b/assets/icons/caretLeft.png new file mode 100644 index 00000000..472a059d Binary files /dev/null and b/assets/icons/caretLeft.png differ diff --git a/assets/icons/caretLeft@2x.png b/assets/icons/caretLeft@2x.png new file mode 100644 index 00000000..7a43e7c0 Binary files /dev/null and b/assets/icons/caretLeft@2x.png differ diff --git a/assets/icons/caretLeft@3x.png b/assets/icons/caretLeft@3x.png new file mode 100644 index 00000000..bc460ec3 Binary files /dev/null and b/assets/icons/caretLeft@3x.png differ diff --git a/assets/icons/caretRight.png b/assets/icons/caretRight.png new file mode 100644 index 00000000..1383a535 Binary files /dev/null and b/assets/icons/caretRight.png differ diff --git a/assets/icons/caretRight@2x.png b/assets/icons/caretRight@2x.png new file mode 100644 index 00000000..52a7ed46 Binary files /dev/null and b/assets/icons/caretRight@2x.png differ diff --git a/assets/icons/caretRight@3x.png b/assets/icons/caretRight@3x.png new file mode 100644 index 00000000..0cbfeb26 Binary files /dev/null and b/assets/icons/caretRight@3x.png differ diff --git a/assets/icons/check.png b/assets/icons/check.png new file mode 100644 index 00000000..35cca2fe Binary files /dev/null and b/assets/icons/check.png differ diff --git a/assets/icons/check@2x.png b/assets/icons/check@2x.png new file mode 100644 index 00000000..33e848fa Binary files /dev/null and b/assets/icons/check@2x.png differ diff --git a/assets/icons/check@3x.png b/assets/icons/check@3x.png new file mode 100644 index 00000000..16a1c8d2 Binary files /dev/null and b/assets/icons/check@3x.png differ diff --git a/assets/icons/demo/clap.png b/assets/icons/demo/clap.png new file mode 100644 index 00000000..a3b4af0b Binary files /dev/null and b/assets/icons/demo/clap.png differ diff --git a/assets/icons/demo/clap@2x.png b/assets/icons/demo/clap@2x.png new file mode 100644 index 00000000..40da2290 Binary files /dev/null and b/assets/icons/demo/clap@2x.png differ diff --git a/assets/icons/demo/clap@3x.png b/assets/icons/demo/clap@3x.png new file mode 100644 index 00000000..b83eefab Binary files /dev/null and b/assets/icons/demo/clap@3x.png differ diff --git a/assets/icons/demo/community.png b/assets/icons/demo/community.png new file mode 100644 index 00000000..714ba679 Binary files /dev/null and b/assets/icons/demo/community.png differ diff --git a/assets/icons/demo/community@2x.png b/assets/icons/demo/community@2x.png new file mode 100644 index 00000000..19f9dbb5 Binary files /dev/null and b/assets/icons/demo/community@2x.png differ diff --git a/assets/icons/demo/community@3x.png b/assets/icons/demo/community@3x.png new file mode 100644 index 00000000..087a2ac5 Binary files /dev/null and b/assets/icons/demo/community@3x.png differ diff --git a/assets/icons/demo/components.png b/assets/icons/demo/components.png new file mode 100644 index 00000000..103330d0 Binary files /dev/null and b/assets/icons/demo/components.png differ diff --git a/assets/icons/demo/components@2x.png b/assets/icons/demo/components@2x.png new file mode 100644 index 00000000..9f8e6944 Binary files /dev/null and b/assets/icons/demo/components@2x.png differ diff --git a/assets/icons/demo/components@3x.png b/assets/icons/demo/components@3x.png new file mode 100644 index 00000000..c18bc027 Binary files /dev/null and b/assets/icons/demo/components@3x.png differ diff --git a/assets/icons/demo/debug.png b/assets/icons/demo/debug.png new file mode 100644 index 00000000..1bbad8bd Binary files /dev/null and b/assets/icons/demo/debug.png differ diff --git a/assets/icons/demo/debug@2x.png b/assets/icons/demo/debug@2x.png new file mode 100644 index 00000000..4a2737e9 Binary files /dev/null and b/assets/icons/demo/debug@2x.png differ diff --git a/assets/icons/demo/debug@3x.png b/assets/icons/demo/debug@3x.png new file mode 100644 index 00000000..ca490b9b Binary files /dev/null and b/assets/icons/demo/debug@3x.png differ diff --git a/assets/icons/demo/github.png b/assets/icons/demo/github.png new file mode 100644 index 00000000..5acd2cde Binary files /dev/null and b/assets/icons/demo/github.png differ diff --git a/assets/icons/demo/github@2x.png b/assets/icons/demo/github@2x.png new file mode 100644 index 00000000..96a933b1 Binary files /dev/null and b/assets/icons/demo/github@2x.png differ diff --git a/assets/icons/demo/github@3x.png b/assets/icons/demo/github@3x.png new file mode 100644 index 00000000..bc865d83 Binary files /dev/null and b/assets/icons/demo/github@3x.png differ diff --git a/assets/icons/demo/heart.png b/assets/icons/demo/heart.png new file mode 100644 index 00000000..ae0c102e Binary files /dev/null and b/assets/icons/demo/heart.png differ diff --git a/assets/icons/demo/heart@2x.png b/assets/icons/demo/heart@2x.png new file mode 100644 index 00000000..3c54d211 Binary files /dev/null and b/assets/icons/demo/heart@2x.png differ diff --git a/assets/icons/demo/heart@3x.png b/assets/icons/demo/heart@3x.png new file mode 100644 index 00000000..6fb35810 Binary files /dev/null and b/assets/icons/demo/heart@3x.png differ diff --git a/assets/icons/demo/pin.png b/assets/icons/demo/pin.png new file mode 100644 index 00000000..43987e27 Binary files /dev/null and b/assets/icons/demo/pin.png differ diff --git a/assets/icons/demo/pin@2x.png b/assets/icons/demo/pin@2x.png new file mode 100644 index 00000000..f1fc669b Binary files /dev/null and b/assets/icons/demo/pin@2x.png differ diff --git a/assets/icons/demo/pin@3x.png b/assets/icons/demo/pin@3x.png new file mode 100644 index 00000000..ad406cef Binary files /dev/null and b/assets/icons/demo/pin@3x.png differ diff --git a/assets/icons/demo/podcast.png b/assets/icons/demo/podcast.png new file mode 100644 index 00000000..32723e12 Binary files /dev/null and b/assets/icons/demo/podcast.png differ diff --git a/assets/icons/demo/podcast@2x.png b/assets/icons/demo/podcast@2x.png new file mode 100644 index 00000000..a4b8e770 Binary files /dev/null and b/assets/icons/demo/podcast@2x.png differ diff --git a/assets/icons/demo/podcast@3x.png b/assets/icons/demo/podcast@3x.png new file mode 100644 index 00000000..83c3dc3b Binary files /dev/null and b/assets/icons/demo/podcast@3x.png differ diff --git a/assets/icons/demo/slack.png b/assets/icons/demo/slack.png new file mode 100644 index 00000000..fa2c643d Binary files /dev/null and b/assets/icons/demo/slack.png differ diff --git a/assets/icons/demo/slack@2x.png b/assets/icons/demo/slack@2x.png new file mode 100644 index 00000000..b4706611 Binary files /dev/null and b/assets/icons/demo/slack@2x.png differ diff --git a/assets/icons/demo/slack@3x.png b/assets/icons/demo/slack@3x.png new file mode 100644 index 00000000..7ce15909 Binary files /dev/null and b/assets/icons/demo/slack@3x.png differ diff --git a/assets/icons/hidden.png b/assets/icons/hidden.png new file mode 100644 index 00000000..46e0d395 Binary files /dev/null and b/assets/icons/hidden.png differ diff --git a/assets/icons/hidden@2x.png b/assets/icons/hidden@2x.png new file mode 100644 index 00000000..694c89c7 Binary files /dev/null and b/assets/icons/hidden@2x.png differ diff --git a/assets/icons/hidden@3x.png b/assets/icons/hidden@3x.png new file mode 100644 index 00000000..aaea5058 Binary files /dev/null and b/assets/icons/hidden@3x.png differ diff --git a/assets/icons/ladybug.png b/assets/icons/ladybug.png new file mode 100644 index 00000000..dc62a4f1 Binary files /dev/null and b/assets/icons/ladybug.png differ diff --git a/assets/icons/ladybug@2x.png b/assets/icons/ladybug@2x.png new file mode 100644 index 00000000..f636eae9 Binary files /dev/null and b/assets/icons/ladybug@2x.png differ diff --git a/assets/icons/ladybug@3x.png b/assets/icons/ladybug@3x.png new file mode 100644 index 00000000..3987bed7 Binary files /dev/null and b/assets/icons/ladybug@3x.png differ diff --git a/assets/icons/lock.png b/assets/icons/lock.png new file mode 100644 index 00000000..d0f72807 Binary files /dev/null and b/assets/icons/lock.png differ diff --git a/assets/icons/lock@2x.png b/assets/icons/lock@2x.png new file mode 100644 index 00000000..993502f8 Binary files /dev/null and b/assets/icons/lock@2x.png differ diff --git a/assets/icons/lock@3x.png b/assets/icons/lock@3x.png new file mode 100644 index 00000000..84f0d958 Binary files /dev/null and b/assets/icons/lock@3x.png differ diff --git a/assets/icons/menu.png b/assets/icons/menu.png new file mode 100644 index 00000000..dc80f7d3 Binary files /dev/null and b/assets/icons/menu.png differ diff --git a/assets/icons/menu@2x.png b/assets/icons/menu@2x.png new file mode 100644 index 00000000..d4171831 Binary files /dev/null and b/assets/icons/menu@2x.png differ diff --git a/assets/icons/menu@3x.png b/assets/icons/menu@3x.png new file mode 100644 index 00000000..5dda07ce Binary files /dev/null and b/assets/icons/menu@3x.png differ diff --git a/assets/icons/more.png b/assets/icons/more.png new file mode 100644 index 00000000..cb37e5d9 Binary files /dev/null and b/assets/icons/more.png differ diff --git a/assets/icons/more@2x.png b/assets/icons/more@2x.png new file mode 100644 index 00000000..f0271421 Binary files /dev/null and b/assets/icons/more@2x.png differ diff --git a/assets/icons/more@3x.png b/assets/icons/more@3x.png new file mode 100644 index 00000000..49914943 Binary files /dev/null and b/assets/icons/more@3x.png differ diff --git a/assets/icons/settings.png b/assets/icons/settings.png new file mode 100644 index 00000000..50765da9 Binary files /dev/null and b/assets/icons/settings.png differ diff --git a/assets/icons/settings@2x.png b/assets/icons/settings@2x.png new file mode 100644 index 00000000..10c4e5dd Binary files /dev/null and b/assets/icons/settings@2x.png differ diff --git a/assets/icons/settings@3x.png b/assets/icons/settings@3x.png new file mode 100644 index 00000000..5251198b Binary files /dev/null and b/assets/icons/settings@3x.png differ diff --git a/assets/icons/view.png b/assets/icons/view.png new file mode 100644 index 00000000..f7b76872 Binary files /dev/null and b/assets/icons/view.png differ diff --git a/assets/icons/view@2x.png b/assets/icons/view@2x.png new file mode 100644 index 00000000..85912da2 Binary files /dev/null and b/assets/icons/view@2x.png differ diff --git a/assets/icons/view@3x.png b/assets/icons/view@3x.png new file mode 100644 index 00000000..540aa9ab Binary files /dev/null and b/assets/icons/view@3x.png differ diff --git a/assets/icons/x.png b/assets/icons/x.png new file mode 100644 index 00000000..55e929e7 Binary files /dev/null and b/assets/icons/x.png differ diff --git a/assets/icons/x@2x.png b/assets/icons/x@2x.png new file mode 100644 index 00000000..9aa3a974 Binary files /dev/null and b/assets/icons/x@2x.png differ diff --git a/assets/icons/x@3x.png b/assets/icons/x@3x.png new file mode 100644 index 00000000..c9a03ff7 Binary files /dev/null and b/assets/icons/x@3x.png differ diff --git a/docs/DocsNewNavigation.md b/docs/DocsNewNavigation.md deleted file mode 100644 index bddf319e..00000000 --- a/docs/DocsNewNavigation.md +++ /dev/null @@ -1,30 +0,0 @@ -# New Navigation Spec - -We're introducing a Stack Navigator to manage navigation within the Onyx app. This document outlines the planned changes. - -**Current Implementation:** -The BottomButtons component currently renders individual buttons for text input, voice input, accessing repos, copying conversation text, and clearing the chat history. These buttons trigger specific actions rather than navigating between screens. - -**Proposed Changes:** -Introduce a Stack Navigator to manage the overall navigation flow. The existing functionalities of the BottomButtons component will be integrated into this new navigation structure. - -**Technical Details:** - -* **Library:** React Navigation's Stack Navigator will be used. -* **Components Affected:** BottomButtons.tsx, and potentially other components that handle navigation or related actions. -* **Screens:** New screens will be created for functionalities currently handled by button actions (e.g., a dedicated Repos screen, a Settings screen). -* **Navigation Logic:** The navigation logic will be updated to use the Stack Navigator's API (push, pop, navigate). Button presses in the BottomButtons component will trigger navigation actions rather than direct function calls. - -**Benefits:** - -* Improved navigation structure and flow. -* Clearer separation of concerns between components. -* Enhanced user experience with consistent navigation patterns. -* Easier implementation of future features and screens. - -**Next Steps:** - -1. Implement the Stack Navigator in the app's main navigation structure. -2. Create new screens for functionalities currently handled by the BottomButtons component. -3. Update the BottomButtons component to trigger navigation actions. -4. Test the new navigation thoroughly. diff --git a/docs/chatbar.md b/docs/chatbar.md new file mode 100644 index 00000000..8be12ed3 --- /dev/null +++ b/docs/chatbar.md @@ -0,0 +1,72 @@ +# ChatBar Component Behavior Requirements + +## Voice Recording + +### Recording Controls +- Mic button toggles recording state +- Send button (arrow) should be white and enabled during recording +- Components should be disabled during processing state +- Mic button should be a perfect 36x36 circle with centered icon +- Send button should be a perfect 28x28 circle with centered icon + +### Recording Modes +1. **Normal Recording** (Mic Button) + - Start recording with mic button + - Stop recording with mic button + - Transcribed text appears in input field + - If there's existing text, new transcription is appended with a space + - Does not auto-focus input or show keyboard after transcription + +2. **Immediate Send** (Arrow Button) + - Start recording with mic button + - Hit send button while recording + - Transcription is combined with any existing text and sent immediately + - Input is cleared after sending + - Keyboard stays in current state (shown/hidden) + +### Keyboard Behavior +- Keyboard should not be dismissed when starting recording +- Keyboard should not auto-show after transcription +- Keyboard should only be dismissed when sending typed text +- Expanded state should be preserved during recording + +### Visual Feedback +- Mic button turns white when recording +- Send button turns white during recording or when text is present +- Small thinking animation appears inline at end of text during processing +- Processing animation should not affect layout or push content +- Thinking animation should be 16x16 and aligned with text baseline + +### Text Handling +- All transcribed text should be trimmed of extra whitespace +- When appending transcriptions, maintain single space between segments +- Empty or whitespace-only text should not enable send button +- When sending immediately, combine existing text with new transcription +- Clear input after successful immediate send + +## Input Field Behavior + +### Height Behavior +- Input field should dynamically resize based on content in both expanded and collapsed states +- Container uses minHeight: 50px with 8px vertical padding +- Input area uses minHeight: 34px +- Content maximum height of 240px +- Height changes should be smooth and maintain layout +- Same height behavior whether keyboard is shown or hidden + +### Text Input +- Multiline input enabled +- Maintains proper line breaks +- Scrollable when content exceeds maximum height +- Preserves cursor position during height changes +- Opacity slightly reduced when not expanded +- Placeholder text vertically centered when single line + +### Layout +- Maintains proper padding and margins in all states +- Preserves safe area insets +- Proper vertical alignment with mic and send buttons +- Consistent spacing between elements regardless of height +- 14px horizontal padding on container +- 16px spacing between mic button and input +- 12px spacing between input and send button \ No newline at end of file diff --git a/docs/coder-settings.md b/docs/coder-settings.md new file mode 100644 index 00000000..cca2db7d --- /dev/null +++ b/docs/coder-settings.md @@ -0,0 +1,168 @@ +# Onyx Coder Settings + +The Onyx Coder is a powerful tool that allows you to analyze and modify codebases directly through the app. This document explains the various settings and configurations available. + +## GitHub Authentication + +### GitHub Tokens +You can manage multiple GitHub Personal Access Tokens (PATs) in the settings: + +- **Adding Tokens**: Click "Add Token" to add a new GitHub PAT + - Provide a memorable name for the token + - Enter the GitHub PAT + - The token will be securely stored + +- **Managing Tokens**: + - **Edit**: Modify the name or token value + - **Remove**: Delete a token from the app + - **Active Token**: Click a token to make it active - this token will be used for GitHub operations + - For security, tokens are displayed in truncated form (e.g., `ghp1...f4d2`) + +### Legacy Token Support +For backward compatibility, the system maintains support for existing tokens: + +- **Legacy Migration**: If you had a token before the multi-token update: + - Your token is automatically migrated as "Legacy Token" + - It remains active and functional + - You can continue using the old token or add new ones + +- **Token Synchronization**: + - The system maintains both old and new token storage + - Changes to the legacy token update the new system + - Changes to the active token in the new system update the legacy token + - This ensures smooth transition and no disruption to existing functionality + +### Creating GitHub Tokens +1. Go to GitHub Settings > Developer Settings > Personal Access Tokens +2. Generate a new token with these permissions: + - `repo` (Full control of private repositories) + - `read:org` (Read organization data) + - `read:user` (Read user data) + +## Repository Management + +### Connected Repositories +You can connect multiple repositories to Onyx: + +- **Adding Repositories**: + - Click "Add Repo" + - Enter: + - Owner (GitHub username or organization) + - Repository name + - Branch name + +- **Managing Repositories**: + - **Edit**: Modify repository details + - **Remove**: Disconnect a repository + - **Active Repository**: Click a repository to make it active + - The active repository is highlighted and will be the target of code operations + +### Repository Structure +Each repository connection includes: +- Owner: The GitHub user or organization that owns the repo +- Name: The repository name +- Branch: The specific branch to work with + +## Available Tools + +The coder includes several tools that can be toggled on/off: + +- **View File**: Read file contents from repositories +- **View Hierarchy**: Browse repository file structure +- **Create File**: Create new files in repositories +- **Rewrite File**: Modify existing files +- **Delete File**: Remove files from repositories +- **Post GitHub Comment**: Comment on GitHub issues +- **Fetch Commit Contents**: View file contents from specific commits +- **Scrape Webpage**: Extract content from web pages + +Each tool can be enabled/disabled independently to customize the coder's capabilities. + +## Usage Notes + +1. **Token Security**: + - Tokens are stored securely in the app + - Never share or expose your GitHub tokens + - Use tokens with minimum required permissions + - Rotate tokens periodically for security + +2. **Repository Access**: + - The active token must have access to the repositories you connect + - Private repositories require appropriate token permissions + - Organization repositories may require additional permissions + +3. **Branch Management**: + - Always specify the correct branch for repository operations + - Changes are made to the specified branch only + - Consider using feature branches for experimental changes + +4. **Tool Selection**: + - Enable only the tools you need + - Tools can be toggled at any time + - Some tools may require specific token permissions + +## Best Practices + +1. **Token Management**: + - Use descriptive names for tokens (e.g., "Personal Projects", "Work Repos") + - Create separate tokens for different purposes + - Regularly review and remove unused tokens + - Consider migrating legacy tokens to named tokens for better organization + +2. **Repository Organization**: + - Keep the repository list focused on active projects + - Remove repositories when no longer needed + - Use consistent branch naming conventions + +3. **Security**: + - Never commit tokens to repositories + - Review token permissions regularly + - Revoke compromised tokens immediately + - Use read-only tokens when possible + +## Technical Implementation + +### Token Storage System +The coder uses a dual storage system for tokens: + +1. **Legacy Storage**: + - Maintains a single token field for backward compatibility + - Automatically syncs with the active token in the new system + - Ensures existing integrations continue to work + +2. **New Token System**: + - Stores multiple named tokens + - Each token has a unique ID, name, and value + - Supports selecting an active token + - Automatically migrates legacy tokens + +3. **Synchronization**: + - When setting an active token, updates legacy storage + - When updating legacy token, updates or creates token in new system + - Maintains consistency between both systems + - Handles token removal and updates in both systems + +## Troubleshooting + +Common issues and solutions: + +1. **Token Issues**: + - Verify token has required permissions + - Check token hasn't expired + - Ensure token is active in GitHub settings + - For legacy tokens, try removing and re-adding as a named token + +2. **Repository Access**: + - Confirm repository exists and is accessible + - Check branch name is correct + - Verify token has access to the repository + +3. **Tool Problems**: + - Ensure required token permissions are granted + - Check if repository is properly connected + - Verify active token and repository are set + +4. **Migration Issues**: + - If legacy token isn't showing up, try re-entering it + - If new tokens aren't working, check active token selection + - Verify both legacy and new token storage are properly synced \ No newline at end of file diff --git a/docs/gemini-function-calling.md b/docs/gemini-function-calling.md deleted file mode 100644 index beebefcd..00000000 --- a/docs/gemini-function-calling.md +++ /dev/null @@ -1,319 +0,0 @@ -# Gemini Function Calling Integration - -This document describes how we implement and use Gemini's function calling capabilities in Onyx. - -## Overview - -Function calling allows Gemini to interact with external tools and APIs in a structured way. The model doesn't directly execute functions but generates structured output specifying which functions to call with what arguments. - -## Implementation - -### Core Components - -1. **Function Declarations** -```typescript -interface FunctionDeclaration { - name: string - description: string - parameters: { - type: string - properties: Record - required?: string[] - } -} -``` - -2. **Tool Configuration** -```typescript -interface ToolConfig { - function_calling_config: { - mode: "AUTO" | "ANY" | "NONE" - allowed_function_names?: string[] - } -} -``` - -3. **Function Call Response** -```typescript -interface FunctionCall { - name: string - args: Record -} -``` - -### Integration with Tool Store - -Our tool system integrates with Gemini's function calling through: - -1. **Tool Registration** -```typescript -// Convert Tool to FunctionDeclaration -const toolToFunctionDeclaration = (tool: Tool): FunctionDeclaration => ({ - name: tool.name, - description: tool.description, - parameters: { - type: "object", - properties: tool.parameters, - required: Object.entries(tool.parameters) - .filter(([_, param]) => param.required) - .map(([name]) => name) - } -}) -``` - -2. **Function Call Handling** -```typescript -const handleFunctionCall = async (functionCall: FunctionCall) => { - const tool = toolStore.getTool(functionCall.name) - if (!tool) throw new Error(`Tool ${functionCall.name} not found`) - return await tool.execute(functionCall.args) -} -``` - -## Usage - -### Basic Function Calling - -```typescript -const geminiApi = new GeminiChatApi(config) - -const response = await geminiApi.createChatCompletion({ - messages: [{ - role: "user", - content: "Show me the contents of README.md" - }], - tools: [githubTools.viewFile], - tool_config: { - function_calling_config: { - mode: "AUTO" // Let model decide whether to use tools - } - } -}) -``` - -### Forced Function Calling - -```typescript -const response = await geminiApi.createChatCompletion({ - messages: [{ - role: "user", - content: "What files are in the src directory?" - }], - tools: [githubTools.viewHierarchy], - tool_config: { - function_calling_config: { - mode: "ANY", // Force tool use - allowed_function_names: ["view_hierarchy"] - } - } -}) -``` - -### Multi-Turn Conversations - -```typescript -const messages = [ - { - role: "user", - content: "Show me the README and suggest improvements" - }, - { - role: "assistant", - content: null, - function_call: { - name: "view_file", - args: { - path: "README.md", - owner: "OpenAgentsInc", - repo: "onyx", - branch: "main" - } - } - }, - { - role: "function", - name: "view_file", - content: "# Content of README.md..." - } -] - -const response = await geminiApi.createChatCompletion({ - messages, - tools: [githubTools.viewFile], - tool_config: { - function_calling_config: { mode: "AUTO" } - } -}) -``` - -## Function Calling Modes - -1. **AUTO** (Default) - - Model decides whether to use tools - - Best for general use cases - - Example: "Tell me about this codebase" - -2. **ANY** - - Forces model to use tools - - Best when tool use is required - - Example: "Show me the file contents" - -3. **NONE** - - Disables tool use - - Best for pure conversation - - Example: "Explain how functions work" - -## Best Practices - -### 1. Function Declarations - -- Use clear, descriptive names -- Provide detailed descriptions -- Define parameter types strictly -- Include examples in descriptions -- Mark required parameters - -### 2. Error Handling - -```typescript -try { - const result = await handleFunctionCall(functionCall) - return { - role: "function", - name: functionCall.name, - content: JSON.stringify(result) - } -} catch (error) { - return { - role: "function", - name: functionCall.name, - content: JSON.stringify({ - error: true, - message: error.message - }) - } -} -``` - -### 3. Parameter Validation - -```typescript -const validateParams = ( - params: Record, - declaration: FunctionDeclaration -) => { - const { required = [], properties } = declaration.parameters - - // Check required params - for (const param of required) { - if (!(param in params)) { - throw new Error(`Missing required parameter: ${param}`) - } - } - - // Validate types - for (const [name, value] of Object.entries(params)) { - const schema = properties[name] - if (!schema) { - throw new Error(`Unknown parameter: ${name}`) - } - - // Type checking - if (typeof value !== schema.type) { - throw new Error( - `Invalid type for ${name}: expected ${schema.type}, got ${typeof value}` - ) - } - - // Enum validation - if (schema.enum && !schema.enum.includes(value as string)) { - throw new Error( - `Invalid value for ${name}: must be one of ${schema.enum.join(", ")}` - ) - } - } -} -``` - -### 4. Response Formatting - -```typescript -const formatToolResponse = (result: unknown) => { - if (typeof result === "string") return result - return JSON.stringify(result, null, 2) -} -``` - -## Security Considerations - -1. **Parameter Validation** - - Validate all inputs - - Check types strictly - - Sanitize string inputs - - Validate enum values - -2. **Permission Checking** - - Verify tool access rights - - Check repository permissions - - Validate API tokens - - Log access attempts - -3. **Rate Limiting** - - Implement per-tool limits - - Track usage metrics - - Handle rate limit errors - - Add backoff logic - -## Testing - -1. **Unit Tests** -```typescript -describe("function calling", () => { - it("validates parameters correctly", () => { - const declaration = { - name: "test_tool", - parameters: { - required: ["param1"], - properties: { - param1: { type: "string" } - } - } - } - - expect(() => validateParams({}, declaration)) - .toThrow("Missing required parameter") - - expect(() => validateParams({ param1: 123 }, declaration)) - .toThrow("Invalid type") - }) -}) -``` - -2. **Integration Tests** -```typescript -describe("gemini integration", () => { - it("handles function calls", async () => { - const response = await geminiApi.createChatCompletion({ - messages: [{ - role: "user", - content: "Show README.md" - }], - tools: [githubTools.viewFile] - }) - - expect(response.function_call).toBeDefined() - expect(response.function_call.name).toBe("view_file") - }) -}) -``` - -## Resources - -- [Gemini Function Calling Documentation](https://ai.google.dev/docs/function_calling) -- [OpenAPI Schema Specification](https://spec.openapis.org/oas/v3.0.3#schema) -- [JSON Schema](https://json-schema.org/understanding-json-schema/) \ No newline at end of file diff --git a/docs/gemini.md b/docs/gemini.md deleted file mode 100644 index e90d1be9..00000000 --- a/docs/gemini.md +++ /dev/null @@ -1,251 +0,0 @@ -# Gemini Integration with Tool Use - -This document describes the Gemini API integration in Onyx, with a focus on tool use capabilities and implementation plan. - -## Overview - -The Gemini integration provides advanced language model capabilities with tool use through Google's Vertex AI. The integration is designed to be modular and extensible, supporting various tools that enhance the model's capabilities. - -## Configuration - -### Environment Variables - -```bash -GOOGLE_CLOUD_PROJECT=your_project_id -GOOGLE_CLOUD_REGION=your_region # defaults to us-central1 -``` - -### Default Configuration - -```typescript -const DEFAULT_CONFIG: GeminiConfig = { - project: Config.GOOGLE_CLOUD_PROJECT ?? "", - location: Config.GOOGLE_CLOUD_REGION ?? "us-central1", - model: "gemini-2.0-flash-exp", -} -``` - -## Implementation Plan - -### Phase 1: Basic Tool Integration - -#### File Structure - -``` -app/services/gemini/ -├── gemini-api.types.ts - Type definitions -├── gemini-chat.ts - Main API implementation -├── tools/ - Tool implementations -│ ├── github.ts - GitHub tools -│ ├── index.ts - Tool exports -│ └── types.ts - Tool type definitions -└── index.ts - Exports -``` - -#### Core Components - -1. **Type Definitions** (`gemini-api.types.ts`): - ```typescript - interface GeminiConfig { - project: string - location?: string - model?: string - } - - interface Tool { - name: string - description: string - parameters: Record - execute: (params: Record) => Promise - } - - interface ChatMessage { - role: "user" | "assistant" | "system" - content: string - tools?: Tool[] - } - ``` - -2. **GitHub Tools** (`tools/github.ts`): - ```typescript - export const githubTools = { - viewFile: { - name: "view_file", - description: "View file contents at path", - parameters: { - path: "string", - owner: "string", - repo: "string", - branch: "string" - }, - execute: async (params) => { - // Implementation - } - }, - viewHierarchy: { - name: "view_hierarchy", - description: "View file/folder hierarchy at path", - parameters: { - path: "string", - owner: "string", - repo: "string", - branch: "string" - }, - execute: async (params) => { - // Implementation - } - } - } - ``` - -3. **Chat Implementation** (`gemini-chat.ts`): - ```typescript - export class GeminiChatApi { - constructor(config: GeminiConfig) - - async createChatCompletion( - messages: ChatMessage[], - tools?: Tool[], - options?: GenerateContentConfig - ): Promise - - private handleToolCalls( - toolCalls: ToolCall[] - ): Promise - } - ``` - -### Phase 2: Advanced Tool Features - -1. **Tool Categories**: - - Repository Management - - File Operations - - Code Analysis - - External Services - -2. **Tool Validation**: - - Parameter type checking - - Permission validation - - Rate limiting - -3. **Tool Response Handling**: - - Structured responses - - Error handling - - Response formatting - -### Phase 3: Future Tools - -1. **Code Analysis Tools**: - - Code review - - Security scanning - - Performance analysis - - Dependency checking - -2. **Repository Tools**: - - Branch management - - PR workflows - - Issue management - - Repository statistics - -3. **External Service Tools**: - - Documentation generation - - API integration - - Testing frameworks - - Deployment tools - -## Usage Examples - -### Basic Tool Use - -```typescript -const geminiApi = new GeminiChatApi(config) - -const response = await geminiApi.createChatCompletion({ - messages: [{ - role: "user", - content: "Show me the contents of README.md" - }], - tools: [githubTools.viewFile], - options: { - temperature: 0.7 - } -}) -``` - -### Multiple Tools - -```typescript -const response = await geminiApi.createChatCompletion({ - messages: [{ - role: "user", - content: "Analyze the project structure and suggest improvements" - }], - tools: [ - githubTools.viewFile, - githubTools.viewHierarchy, - codeAnalysisTools.analyzeStructure - ] -}) -``` - -## Error Handling - -1. **Tool-specific Errors**: - - Invalid parameters - - Permission denied - - Resource not found - - Rate limits exceeded - -2. **API Errors**: - - Authentication failures - - Network issues - - Invalid responses - - Timeout handling - -3. **Response Processing**: - - Validation - - Formatting - - Error recovery - -## Best Practices - -1. **Tool Design**: - - Clear descriptions - - Validated parameters - - Consistent error handling - - Comprehensive documentation - -2. **Security**: - - Token validation - - Permission checking - - Rate limiting - - Audit logging - -3. **Performance**: - - Caching where appropriate - - Batch operations - - Async processing - - Resource cleanup - -## Testing - -1. **Unit Tests**: - - Tool validation - - Parameter checking - - Response formatting - -2. **Integration Tests**: - - API communication - - Tool execution - - Error scenarios - -3. **End-to-end Tests**: - - Complete workflows - - Multiple tool interactions - - Real-world scenarios - -## Resources - -- [Gemini API Documentation](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini) -- [Vertex AI Documentation](https://cloud.google.com/vertex-ai/docs) -- [Tool Use Guidelines](https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/function-calling) \ No newline at end of file diff --git a/docs/github-tools.md b/docs/github-tools.md deleted file mode 100644 index 54076a24..00000000 --- a/docs/github-tools.md +++ /dev/null @@ -1,133 +0,0 @@ -# GitHub Tools Integration - -Onyx supports integration with GitHub through a set of tools that allow the AI to read files and navigate repositories. This document explains how to set up and use these features. - -## Available Tools - -The AI has access to two GitHub-related tools: - -1. `view_file` - View the contents of a file in a GitHub repository -2. `view_folder` - List the contents of a directory in a GitHub repository - -## Configuration - -### Setting up GitHub Token - -To use the GitHub tools, you need to provide a GitHub Personal Access Token. This can be done through the Configure modal: - -1. Tap the Configure button (gear icon) in the bottom toolbar -2. In the Configure modal, find the "GitHub Token" section -3. Enter your GitHub Personal Access Token -4. Tap Save - -The token is stored securely in the app's state management system and will be used for all GitHub-related operations. - -### Enabling/Disabling Tools - -You can toggle the AI's access to tools: - -1. Open the Configure modal -2. Find the "Tools" section -3. Tap the toggle button to enable/disable tools -4. Tap Save - -When tools are disabled, the AI will not have access to GitHub operations. - -## Using GitHub Tools - -Once configured, you can ask the AI to: - -- View specific files: "Show me the README.md file from repository X" -- List directory contents: "What files are in the src directory of repository Y?" -- Navigate codebases: "Help me understand the structure of repository Z" - -The AI will automatically use the appropriate tool to fulfill your request. - -## Technical Implementation - -### Store Integration - -The GitHub token and tools state are managed in the ChatStore: - -```typescript -export const ChatStoreModel = types - .model("ChatStore") - .props({ - githubToken: types.optional(types.string, ""), - toolsEnabled: types.optional(types.boolean, true), - }) - .actions((self) => ({ - setGithubToken(token: string) { - self.githubToken = token - }, - setToolsEnabled(enabled: boolean) { - self.toolsEnabled = enabled - } - })) -``` - -### Request Format - -When making requests to the AI, the app includes the GitHub token and tools configuration: - -```typescript -const chatConfig = { - body: { - ...(toolsEnabled && { - githubToken, - tools: ["view_file", "view_folder"] - }) - } -} -``` - -### UI Components - -The configuration interface is implemented in the ConfigureModal component, which provides: - -- Secure token input field -- Tools toggle switch -- Save/Cancel functionality - -## Security Considerations - -- The GitHub token is stored only in memory and app state -- Token input uses secure text entry -- Token is never displayed after entry -- Tools can be disabled to prevent unintended GitHub access - -## Best Practices - -1. Use a GitHub token with minimal required permissions -2. Enable tools only when needed -3. Monitor the AI's tool usage through the chat interface -4. Clear tokens when switching users or logging out - -## Troubleshooting - -Common issues and solutions: - -1. **Tools not working** - - Check if tools are enabled in Configure modal - - Verify GitHub token is entered correctly - - Ensure token has required permissions - -2. **Cannot access private repositories** - - Verify token has private repository access - - Check repository name and owner are correct - -3. **Token not saving** - - Make sure to tap Save after entering token - - Check for any error messages - - Verify app has necessary storage permissions - -## Future Enhancements - -Planned improvements to the GitHub tools integration: - -1. Support for writing files and creating pull requests -2. Repository search functionality -3. Commit history viewing -4. Issue and PR management -5. Token permission scope visualization -6. Multiple token management for different repositories \ No newline at end of file diff --git a/docs/groq.md b/docs/groq.md deleted file mode 100644 index 2e0bee4b..00000000 --- a/docs/groq.md +++ /dev/null @@ -1,222 +0,0 @@ -# Groq API Integration - -This document describes the Groq API integration in Onyx, including the current implementation and guidelines for future expansions. - -## Overview - -The Groq integration provides high-performance language model capabilities through Groq's cloud API. The integration is designed to be modular and extensible, currently supporting chat completions with potential for expansion to other Groq services. - -## Configuration - -### Environment Variables - -```bash -GROQ_API_KEY=your_api_key_here -``` - -The API key can be obtained from the [Groq Console](https://console.groq.com/keys). - -### Default Configuration - -```typescript -const DEFAULT_CONFIG: GroqConfig = { - apiKey: Config.GROQ_API_KEY ?? "", - baseURL: "https://api.groq.com/v1", - timeout: 30000, -} -``` - -## Current Implementation - -### File Structure - -``` -app/services/groq/ -├── groq-api.types.ts - Type definitions -├── groq-chat.ts - Main API implementation -└── index.ts - Exports -``` - -### Core Components - -1. **Type Definitions** (`groq-api.types.ts`): - - - `GroqConfig` - API configuration interface - - `ChatMessage` - Message format for chat completions - - `ChatCompletionResponse` - Response format from chat endpoints - -2. **API Implementation** (`groq-chat.ts`): - - - `GroqChatApi` class handling API interactions - - Error handling and response processing - - Message format conversion - -3. **Store Integration** (`app/models/chat/`): - - `ChatStore` with Groq actions - - Message state management - - Error handling and loading states - -### Usage in Components - -#### Text Input - -```typescript -const { chatStore } = useStores() -await chatStore.sendMessage(messageText) -``` - -#### Voice Input - -```typescript -const { chatStore } = useStores() -await chatStore.sendMessage(transcribedText) -``` - -## Available Endpoints - -### Currently Implemented - -#### Chat Completions - -- Endpoint: `/chat/completions` -- Models: - - `lllama-3.1-8b-instant` (default) - - Other Groq models as they become available -- Parameters: - ```typescript - { - temperature?: number // Default: 0.7 - max_tokens?: number // Default: 1024 - top_p?: number // Default: 1 - stop?: string | string[] - response_format?: { type: "json_object" } - } - ``` - -### Future Expansion Opportunities - -#### Speech-to-Text - -Groq's speech-to-text API could be integrated to replace or supplement the current local voice transcription: - -```typescript -interface SpeechToTextConfig { - model: string - audio: ArrayBuffer - language?: string - task?: "transcribe" | "translate" -} - -// Potential implementation -class GroqSpeechApi { - async transcribe(audio: ArrayBuffer, config?: Partial) { - // Implementation - } -} -``` - -#### Text-to-Speech - -If Groq adds text-to-speech capabilities: - -```typescript -interface TextToSpeechConfig { - model: string - text: string - voice?: string - speed?: number -} - -// Potential implementation -class GroqSpeechApi { - async synthesize(text: string, config?: Partial) { - // Implementation - } -} -``` - -## Error Handling - -The integration includes comprehensive error handling: - -1. **API Level**: - - - Network errors - - Authentication errors - - Rate limiting - - Invalid responses - -2. **Store Level**: - - - Message state management - - Error state propagation - - Loading states - -3. **UI Level**: - - Error messages - - Loading indicators - - Retry mechanisms - -## Best Practices - -1. **API Calls**: - - - Always use the store actions rather than calling the API directly - - Handle errors at the appropriate level - - Use proper typing for all API interactions - -2. **Configuration**: - - - Keep API keys secure - - Use environment variables for configuration - - Set appropriate timeouts - -3. **Error Handling**: - - Log errors appropriately - - Provide user-friendly error messages - - Include retry mechanisms where appropriate - -## Adding New Endpoints - -To add support for new Groq endpoints: - -1. Add appropriate types to `groq-api.types.ts` -2. Create a new service class or extend existing ones -3. Add store actions if needed -4. Update documentation - -Example: - -```typescript -// 1. Add types -interface NewEndpointConfig { - // ... -} - -// 2. Create service -class GroqNewFeatureApi { - // ... -} - -// 3. Add store actions -const withNewFeatureActions = (self: Instance) => ({ - newFeatureAction: flow(function* () { - // ... - }), -}) -``` - -## Testing - -When implementing new features: - -1. Add unit tests for API interactions -2. Add integration tests for store actions -3. Test error handling -4. Test edge cases and rate limiting - -## Resources - -- [Groq API Documentation](https://console.groq.com/docs/api-reference) -- [Groq Models](https://console.groq.com/docs/models) -- [Rate Limits](https://console.groq.com/docs/rate-limits) diff --git a/docs/hierarchy.md b/docs/hierarchy.md index 29247c40..dddb236f 100644 --- a/docs/hierarchy.md +++ b/docs/hierarchy.md @@ -13,7 +13,25 @@ ├── types.ts ├── chat ├── Chat.tsx + ├── ChatBar.tsx + ├── ChatDrawerContainer.tsx ├── ChatDrawerContent.tsx + ├── ChatOverlay.tsx + ├── markdown + ├── MessageContent.tsx + ├── ToolInvocation.tsx + ├── index.ts + ├── styles.ts + ├── styles.ts + ├── components + ├── AutoImage.tsx + ├── Header.tsx + ├── Icon.tsx + ├── KeyboardDismisser.tsx + ├── Screen.tsx + ├── Text.tsx + ├── ThinkingAnimation.tsx + ├── index.ts ├── config ├── config.base.ts ├── config.dev.ts @@ -26,7 +44,11 @@ ├── ReactotronConfig.ts ├── hooks ├── useAutoUpdate.ts + ├── useChat.ts + ├── useHeader.tsx + ├── useKeyboard.ts ├── useVoicePermissions.ts + ├── useVoiceRecording.ts ├── models ├── RootStore.ts ├── _helpers @@ -35,43 +57,37 @@ ├── useStores.ts ├── withSetPropAction.ts ├── chat - ├── ChatActions.ts + ├── ChatStorage.ts ├── ChatStore.ts ├── index.ts ├── coder ├── CoderStore.ts ├── index.ts - ├── tools - ├── ToolActions.ts - ├── ToolStore.ts - ├── index.ts ├── types ├── repo.ts - ├── onyx - ├── BottomButtons.styles.ts - ├── BottomButtons.tsx - ├── ChatOverlay.tsx - ├── ChatOverlayPrev.tsx - ├── ConfigureModal.tsx - ├── OnyxLayout.tsx - ├── TextInputModal.tsx - ├── VoiceInputModal.styles.ts - ├── VoiceInputModal.tsx - ├── markdown - ├── MessageContent.tsx - ├── MessageContentPrev.tsx - ├── ToolInvocation.tsx - ├── index.ts - ├── styles.ts - ├── repo - ├── RepoSection.tsx - ├── styles.ts - ├── types.ts - ├── styles.ts + ├── navigators + ├── AppNavigator.tsx + ├── index.ts + ├── navigationUtilities.ts ├── screens + ├── ChatScreen + ├── ChatScreen.tsx + ├── index.ts ├── ErrorScreen ├── ErrorBoundary.tsx ├── ErrorDetails.tsx + ├── SettingsScreen + ├── SettingsScreen.tsx + ├── coder + ├── GithubTokenSection.tsx + ├── RepoFormSection.tsx + ├── RepoListSection.tsx + ├── RepoSettings.tsx + ├── ToolsSection.tsx + ├── styles.ts + ├── types.ts + ├── index.ts + ├── index.ts ├── services ├── api ├── api.ts @@ -79,25 +95,20 @@ ├── apiProblem.test.ts ├── apiProblem.ts ├── index.ts - ├── gemini - ├── gemini-api.types.ts - ├── gemini-chat.ts - ├── index.ts - ├── tools - ├── github-impl.ts - ├── github.ts - ├── index.ts - ├── types.ts ├── groq ├── groq-api.types.ts ├── groq-chat.ts ├── index.ts - ├── local-models - ├── LocalModelService.ts - ├── constants.ts ├── theme ├── colors.ts + ├── colorsDark.ts + ├── images.ts ├── index.ts + ├── onyx.ts + ├── spacing.ts + ├── spacingDark.ts + ├── styles.ts + ├── timing.ts ├── typography.ts ├── utils ├── clearStorage.ts @@ -105,48 +116,32 @@ ├── ignore-warnings.ts ├── isEmulator.ts ├── log.ts + ├── polyfills.ts ├── storage ├── index.ts ├── storage.test.ts ├── storage.ts + ├── useAppTheme.ts ├── useIsFocused.ts ├── useIsMounted.ts ├── useSafeAreaInsetsStyle.ts ├── app.config.ts ├── app.json ├── assets - ├── icons - ├── configure.png - ├── text.png - ├── voice.png - ├── images - ├── Thinking-Animation-Orig.gif - ├── Thinking-Animation.gif - ├── app-icon-all.png - ├── splash.png ├── dist ├── docs - ├── DocsNewNavigation.md ├── ai-design-language.md + ├── chat-persistence.md + ├── chatbar.md + ├── coder-settings.md ├── data-marketplace.md - ├── gemini-function-calling.md - ├── gemini.md - ├── github-tools.md ├── groq-voice.md - ├── groq.md ├── init.md - ├── keys.md - ├── llm-store.md - ├── local-models.md ├── markdown.md - ├── model-switching.md ├── onboarding.md - ├── onyx-layout.md ├── permissions.md ├── roadmap-brainstorming.md ├── roadmap.md - ├── tool-component.md - ├── tools.md ├── eas.json ├── ios ├── package.json diff --git a/docs/init.md b/docs/init.md index 19a49fd8..1202379a 100644 --- a/docs/init.md +++ b/docs/init.md @@ -1,4 +1,10 @@ ## App initialization flow -- pacakage.json defines entry: `"main": "src/app.tsx",` -- src/app.tsx +- pacakage.json defines entry: `"main": "app/app.tsx",` +- app/app.tsx + - Imports polyfills + - Sets up EAS auto updates + - Sets up navigation + - Rehydrates store, then hides splash screen + - Includes providers for safearea, error boundary, keyboard + - Wraps AppNavigator diff --git a/docs/keys.md b/docs/keys.md deleted file mode 100644 index e67044c6..00000000 --- a/docs/keys.md +++ /dev/null @@ -1,158 +0,0 @@ -# Key Management in Onyx - -## Overview - -The key management system in Onyx is designed to be secure and modular, with a core KeyService that manages BIP39 mnemonics and derived keys. This service acts as the foundation for other services (like BreezService and NostrService) that need cryptographic keys. - -## Architecture - -### Secure Storage - -The system uses expo-secure-store for sensitive data: - -```typescript -// Secure storage for sensitive data -export const secureStorage = { - getMnemonic: () => SecureStore.getItemAsync('mnemonic'), - setMnemonic: (value: string) => SecureStore.setItemAsync('mnemonic', value), - removeMnemonic: () => SecureStore.deleteItemAsync('mnemonic'), -} -``` - -### KeyService - -The KeyService is a singleton that handles: -1. BIP39 mnemonic generation and secure storage -2. Interface for other services to access keys -3. Validation and security checks - -```typescript -interface KeyServiceConfig { - existingMnemonic?: string -} - -interface KeyService { - initialize(config?: KeyServiceConfig): Promise - getMnemonic(): Promise - isInitialized(): boolean - reset(): Promise -} -``` - -### Service Dependencies - -```mermaid -graph TD - A[ServiceManager] --> B[KeyService] - B --> C[SecureStore] - A --> D[BreezService] - D --> B - A --> E[NostrService] - E --> B -``` - -## Key Derivation - -### Breez Keys -- Uses BIP39 mnemonic directly -- Handles its own derivation internally -- Used for Lightning Network operations - -### Nostr Keys -- Derives keys by hashing the mnemonic -- Uses SHA256 for private key generation -- Generates bech32-encoded npub/nsec -- Used for Nostr protocol operations - -## Initialization Flow - -1. ServiceManager starts initialization -2. KeyService is initialized first - - Loads mnemonic from secure storage if exists - - Generates new mnemonic if needed - - Validates and stores mnemonic securely -3. Other services initialize in parallel: - - BreezService gets mnemonic from KeyService - - NostrService gets mnemonic and derives Nostr keys - -## Security Model - -### Native Layer -- All sensitive data stored in expo-secure-store -- Mnemonic never leaves native context -- Services access keys through KeyService - -### Web/DOM Layer -- No direct access to secure storage -- Keys passed down from native components -- Temporary access only when needed - -### State Management -- Non-sensitive state in zustand with AsyncStorage -- Sensitive data always in SecureStore -- Clear separation of concerns - -## Usage Example - -```typescript -// Native component with access to secure data -function SecureWrapper({ children }) { - const { npub } = useNostr() // Accesses secure storage - - return ( - - {children} - - ) -} - -// DOM component receives only what it needs -function WebComponent() { - const { npub } = useNostrContext() // Gets npub from context - return
Nostr ID: {npub}
-} - -// Usage - - - -``` - -## Security Considerations - -1. Storage Security - - Sensitive data only in SecureStore - - Non-sensitive state in AsyncStorage - - Clear data separation - -2. Key Generation - - Uses cryptographically secure RNG - - Follows BIP39 specification - - Validates all mnemonics - -3. Access Control - - Web components never access secure storage directly - - Keys passed down through React context/props - - Clear boundaries between secure and non-secure code - -## Future Enhancements - -1. Enhanced Security - - Biometric authentication for key access - - Key encryption at rest - - Secure enclave integration where available - -2. Key Management - - Multiple key derivation paths - - Key rotation support - - Backup and recovery - -3. Service Integration - - Plugin system for new services - - Key usage policies - - Access control lists - -4. Web Security - - Memory clearing after use - - Secure key transmission - - Audit logging \ No newline at end of file diff --git a/docs/llm-store.md b/docs/llm-store.md deleted file mode 100644 index 08042890..00000000 --- a/docs/llm-store.md +++ /dev/null @@ -1,229 +0,0 @@ -# LLM Store Documentation - -The LLM Store manages the state and lifecycle of local language models in the Onyx app. It uses MobX-State-Tree for state management and follows a modular architecture for better maintainability. - -## Architecture - -The store is organized into several modules: - -``` -src/models/llm/ -├── actions/ -│ ├── initialize.ts # Initialization logic -│ └── model-management.ts # Model operations (download, delete, etc.) -├── types.ts # Type definitions -├── views.ts # Computed properties -├── store.ts # Main store definition -└── index.ts # Public exports -``` - -## Store State - -The store maintains the following state: - -```typescript -interface ILLMStore extends IStateTreeNode { - isInitialized: boolean - error: string | null - models: IModelInfo[] & { - replace(items: IModelInfo[]): void - } - selectedModelKey: string | null -} -``` - -### ModelInfo Structure - -Each model in the store has the following properties: - -```typescript -interface IModelInfo { - key: string // Unique identifier - displayName: string // Human-readable name - path: string | null // Local file path when downloaded - status: ModelStatus // Current state - progress: number // Download progress (0-100) - error?: string // Error message if any -} -``` - -### Model Status States - -- `idle`: Initial state, model not downloaded -- `downloading`: Model is being downloaded -- `initializing`: Model is being loaded into memory -- `ready`: Model is downloaded and ready to use -- `error`: An error occurred - -## Type System - -The store uses a combination of MobX-State-Tree types and TypeScript interfaces: - -```typescript -// MST Model Definition -const ModelInfoModel = types.model("ModelInfo", { - key: types.string, - displayName: types.string, - path: types.maybeNull(types.string), - status: types.enumeration(["idle", "downloading", "initializing", "ready", "error"]), - progress: types.number, - error: types.maybe(types.string), -}) - -// TypeScript Interface -interface IModelInfo extends Instance {} -``` - -The store extends `IStateTreeNode` to provide access to MST functionality while maintaining type safety. - -## Actions - -### Initialization - -```typescript -const store = createLLMStoreDefaultModel() -await store.initialize() -``` - -The initialize action: -1. Creates the models directory if needed -2. Scans for locally downloaded models -3. Updates store state with found models using MST's `replace` action -4. Automatically selects a ready model if available - -### Model Management - -#### Download Model -```typescript -await store.startModelDownload("1B") -``` -- Starts model download -- Updates progress in real-time -- Handles download errors -- Automatically selects model when ready if none selected - -#### Cancel Download -```typescript -await store.cancelModelDownload("1B") -``` -- Cancels ongoing download -- Cleans up temporary files -- Resets model status to idle - -#### Delete Model -```typescript -await store.deleteModel("1B") -``` -- Removes model file from disk -- Updates store state -- Clears selection if deleted model was selected - -#### Select Model -```typescript -store.selectModel("1B") // Select model -store.selectModel(null) // Clear selection -``` - -## Views - -### selectedModel -```typescript -const model = store.selectedModel -// Returns currently selected ModelInfo or null -``` - -### downloadingModel -```typescript -const downloading = store.downloadingModel -// Returns ModelInfo of downloading model or null -``` - -### hasReadyModel -```typescript -const hasModel = store.hasReadyModel -// Returns true if any model is in ready state -``` - -## Error Handling - -The store maintains an error state that can be monitored: -```typescript -if (store.error) { - console.error("Store error:", store.error) -} -``` - -Errors are set when: -- Initialization fails -- Download fails -- Model deletion fails -- File validation fails - -## File Management - -Models are stored in the app's cache directory: -```typescript -const MODELS_DIR = `${FileSystem.cacheDirectory}models` -``` - -File operations include: -- Download validation (size > 100MB) -- Temporary file cleanup -- Automatic background download cancellation -- Directory creation and maintenance - -## Usage Example - -```typescript -import { createLLMStoreDefaultModel } from "@/models/LLMStore" - -// Create and initialize store -const store = createLLMStoreDefaultModel() -await store.initialize() - -// Start model download -try { - await store.startModelDownload("1B") -} catch (error) { - console.error("Download failed:", error) -} - -// Monitor download progress -reaction( - () => store.downloadingModel?.progress, - progress => { - if (progress !== undefined) { - console.log(`Download progress: ${progress}%`) - } - } -) - -// Use selected model -const model = store.selectedModel -if (model?.status === "ready") { - console.log(`Using model at ${model.path}`) -} -``` - -## Best Practices - -1. Always initialize the store before use -2. Handle errors at the UI level -3. Monitor download progress for user feedback -4. Clean up resources when switching models -5. Validate model files after download -6. Handle app backgrounding gracefully -7. Provide clear error messages to users -8. Use TypeScript interfaces for type safety -9. Follow the modular architecture pattern -10. Document new features and changes - -## Type Safety Notes - -1. The store uses MST's type system for runtime validation -2. TypeScript interfaces provide compile-time type checking -3. Array methods from MST (like `replace`) are properly typed -4. Model status is enforced via TypeScript enum -5. All action parameters are strictly typed -6. Views maintain type safety through interfaces -7. Error handling preserves type information \ No newline at end of file diff --git a/docs/local-models.md b/docs/local-models.md deleted file mode 100644 index 6c55df01..00000000 --- a/docs/local-models.md +++ /dev/null @@ -1,204 +0,0 @@ -# Local Model Management - -The app supports downloading and managing multiple local LLM models. This document explains how the model management system works. - -## Model Configuration - -Models are configured in `src/screens/Chat/constants.ts`: - -```typescript -export const AVAILABLE_MODELS: { [key: string]: ModelConfig } = { - '3B': { - repoId: 'hugging-quants/Llama-3.2-3B-Instruct-Q4_K_M-GGUF', - filename: 'llama-3.2-3b-instruct-q4_k_m.gguf', - displayName: 'Llama 3.2 3B Instruct' - }, - '1B': { - repoId: 'hugging-quants/Llama-3.2-1B-Instruct-Q4_K_M-GGUF', - filename: 'llama-3.2-1b-instruct-q4_k_m.gguf', - displayName: 'Llama 3.2 1B Instruct' - } -} -``` - -## Model Sizes -- 1B model: ~770 MB -- 3B model: ~1.9 GB - -## User Interface - -### Model Manager -- Shows all available models with: - - Model name and size - - Download/Delete button - - Checkmark for active model - - Actual file size when downloaded -- Allows selecting downloaded models -- Allows deleting individual models -- Accessed via "Models" button in chat - -### Chat Screen -- Shows loading indicator during initialization -- Models button in top-right corner -- Chat interface with model commands -- Input disabled when no model loaded - -### Visual States -- Download progress percentage -- Initialization progress -- Error messages with retry option -- Clear state transitions - -## State Management - -Model state is managed through a Zustand store (`src/store/useModelStore.ts`) with the following features: - -### Model States -```typescript -type ModelStatus = 'idle' | 'downloading' | 'initializing' | 'ready' | 'error' | 'releasing' -``` - -State transitions: -1. idle → downloading (when starting download) -2. downloading → initializing (when download completes) -3. initializing → ready (when model is loaded) -4. ready → releasing (when switching models) -5. releasing → initializing (when starting new model) -6. Any state → error (on failure) -7. error → idle (on retry) - -### Store State -```typescript -interface ModelState { - selectedModelKey: string // Current selected model - status: ModelStatus // Current state - progress: number // Download progress (0-100) - modelPath: string | null // Path to downloaded model file - errorMessage: string | null // Error message if any - downloadCancelled: boolean // Whether download was cancelled - needsInitialization: boolean // Whether model needs to be initialized - initializationAttempts: number // Number of initialization attempts - lastDeletedModel: string | null // Track last deleted model -} -``` - -## Model Lifecycle - -### Initial Load -1. Show loading indicator -2. Check for locally downloaded model -3. If found: - - Validate file size (>100MB) - - Initialize automatically -4. If not found or invalid: - - Show model selector - - Reset to idle state - -### Model Download -1. User selects model -2. Shows download confirmation with: - - Size warning - - Background warning - - Wi-Fi recommendation -3. Downloads with progress updates using FileSystem.createDownloadResumable -4. Validates downloaded file: - - Checks file exists - - Verifies minimum size (100MB) - - Validates model info -5. Automatically initializes after download - -### Model Switching -1. User clicks model in manager -2. Current model is released (status: 'releasing') -3. Wait for release to complete -4. Initialize new model (status: 'initializing') -5. Set to ready when complete - -### Error Handling -- Download cancellation (app backgrounded) -- Initialization failures (limited to 1 attempt) -- File validation errors -- Context release errors -- Network issues -- Storage issues -- Memory limit errors: - - For 3B model: Suggests trying 1B model instead - - For 1B model: Suggests contacting support - -## Implementation Details - -### Key Components -- `useModelStore`: Central state management -- `downloadModel`: Handles file downloads with progress -- `useModelContext`: Manages model context -- `useModelInitialization`: Handles initialization flow -- `ModelFileManager`: Model management UI -- `LoadingIndicator`: Initialization progress - -### File Management -- Models stored in app cache directory -- Automatic cleanup on: - - Download cancellation - - Initialization failure - - Model deletion -- File validation before use -- Size verification (>100MB) -- Model info validation -- Progress tracking during download - -### Best Practices -1. Always validate downloaded files -2. Handle app backgrounding gracefully -3. Show clear error messages -4. Allow error recovery -5. Clean up resources properly -6. Prevent invalid state transitions -7. Show clear progress indicators -8. Handle network issues gracefully -9. Limit initialization attempts to 1 -10. Provide clear memory error messages - -## Usage Example - -```typescript -const { - selectedModelKey, // Current model key - status, // Current state - progress, // Download progress - errorMessage, // Current error if any - selectModel, // Switch models - startDownload, // Begin download - setError, // Set error state - reset // Reset to idle state -} = useModelStore() - -// Download with progress example -try { - await downloadModel(repoId, filename, (progress) => { - console.log(`Download progress: ${progress}%`) - }) -} catch (e) { - setError(e.message) - // UI will show error and allow retry -} -``` - -## Technical Notes -- Uses expo-file-system for downloads -- Supports download progress tracking -- Handles app state changes -- Validates file integrity -- Proper error propagation -- State persistence between app launches -- Minimum file size validation (100MB) -- Model info validation -- Progress tracking for both download and initialization -- Clear error messages and recovery paths -- Proper cleanup of resources -- Safe model switching with release handling -- Uses temporary files during download -- Verifies file moves and copies -- Limits initialization attempts to 1 -- Memory-aware initialization with model-specific error messages -- Prevents initialization from idle state -- Only sets downloadCancelled flag for download errors \ No newline at end of file diff --git a/docs/model-switching.md b/docs/model-switching.md deleted file mode 100644 index 239110de..00000000 --- a/docs/model-switching.md +++ /dev/null @@ -1,210 +0,0 @@ -# Model Switching in Onyx - -This document describes the model switching functionality in Onyx, which allows users to switch between different language models (Groq and Gemini) for chat interactions. - -## Overview - -Onyx supports multiple language models through a unified interface, allowing users to switch between models while maintaining a consistent chat experience. The current implementation supports: - -- Groq (llama-3.1-8b-instant) -- Google's Gemini - -## Architecture - -### 1. State Management - -The model selection is managed in the ChatStore using MobX-State-Tree: - -```typescript -const ChatStoreModel = types - .model("ChatStore") - .props({ - // ... other props - activeModel: types.optional(types.enumeration(["groq", "gemini"]), "groq"), - }) - .actions((self) => ({ - // ... other actions - setActiveModel(model: "groq" | "gemini") { - self.activeModel = model - } - })) -``` - -### 2. User Interface - -The model switching interface is implemented through the ConfigureModal component: - -```typescript -export const ConfigureModal = observer(({ visible, onClose }) => { - const { chatStore } = useStores() - - const handleModelChange = (model: "groq" | "gemini") => { - chatStore.setActiveModel(model) - onClose() - } - - // ... render UI -}) -``` - -### 3. Message Processing - -Messages are processed differently depending on the selected model: - -```typescript -if (self.activeModel === "groq") { - result = yield groqChatApi.createChatCompletion( - self.currentMessages, - "llama-3.1-8b-instant", - { - temperature: 0.7, - max_tokens: 1024, - }, - ) -} else { - result = yield geminiChatApi.createChatCompletion( - self.currentMessages, - { - temperature: 0.7, - maxOutputTokens: 1024, - }, - ) -} -``` - -## Model Differences - -### Groq -- Uses llama-3.1-8b-instant model -- Configured with max_tokens parameter -- Faster response times -- More consistent output format - -### Gemini -- Uses Google's Gemini model -- Configured with maxOutputTokens parameter -- Better at complex reasoning -- More capable with multimodal inputs (future feature) - -## Implementation Details - -### 1. Store Configuration - -The ChatStore maintains: -- Current model selection -- Message history -- Generation state -- Error handling - -### 2. UI Components - -The configuration interface includes: -- Model selection buttons -- Visual indication of active model -- Consistent styling with other modals - -### 3. Message Handling - -Each message includes metadata about: -- Which model generated it -- Token usage -- Error states -- Generation status - -## Usage - -### User Perspective - -1. Open the Configure modal using the configure button -2. Select desired model (Groq or Gemini) -3. Continue chat with selected model -4. Model selection persists across sessions - -### Developer Perspective - -1. Access current model: -```typescript -const { chatStore } = useStores() -const currentModel = chatStore.activeModel -``` - -2. Switch models: -```typescript -chatStore.setActiveModel("gemini") -``` - -3. Check message metadata: -```typescript -message.metadata.model // "groq" or "gemini" -message.metadata.tokens // token usage -``` - -## Error Handling - -The system handles various error cases: -- API failures -- Rate limiting -- Invalid responses -- Network issues - -Error messages include: -- Model-specific error codes -- User-friendly messages -- Detailed logging in development - -## Future Enhancements - -1. Additional Models -- Support for more Groq models -- Additional Gemini model variants -- Other API providers - -2. Model Features -- Model-specific temperature controls -- Token limit adjustments -- Specialized prompts per model - -3. UI Improvements -- Model performance metrics -- Cost tracking -- Usage statistics - -## Best Practices - -1. Model Selection -- Choose Groq for faster responses -- Use Gemini for complex reasoning -- Consider token limits for each model - -2. Error Handling -- Always check response status -- Provide clear error messages -- Log detailed errors in development - -3. Message Processing -- Include model info in metadata -- Track token usage -- Handle model-specific response formats - -## Testing - -1. Unit Tests -- Model switching logic -- Error handling -- State management - -2. Integration Tests -- API interactions -- UI functionality -- Error scenarios - -3. End-to-end Tests -- Complete chat flows -- Model switching flows -- Error recovery - -## Resources - -- [Groq API Documentation](https://console.groq.com/docs/api-reference) -- [Gemini API Documentation](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini) -- [MobX-State-Tree Documentation](https://mobx-state-tree.js.org/) \ No newline at end of file diff --git a/docs/onyx-layout.md b/docs/onyx-layout.md deleted file mode 100644 index ccb68ebf..00000000 --- a/docs/onyx-layout.md +++ /dev/null @@ -1,132 +0,0 @@ -# Onyx Layout Documentation - -The Onyx layout system consists of several components that handle text and voice input through modal interfaces. The system is designed to be modular, maintainable, and consistent in styling. - -## Components - -### OnyxLayout - -The main component that handles the overall layout and state management. It displays two buttons at the bottom of the screen for text and voice input. - -**Location**: `src/onyx/OnyxLayout.tsx` - -**Features**: -- Bottom-aligned text and voice input buttons -- State management for both input modals -- Handles sending of text and voice messages - -### TextInputModal - -A full-screen modal component for text input that appears when the text button is pressed. - -**Location**: `src/onyx/TextInputModal.tsx` - -**Features**: -- Full-screen modal with semi-transparent background -- Cancel and Send buttons in header -- Expandable text input field -- Send button activates only when text is entered -- Keyboard-aware layout -- Dismissible via Cancel button or Android back button - -### VoiceInputModal - -A full-screen modal component for voice recording that appears when the voice button is pressed. - -**Location**: `src/onyx/VoiceInputModal.tsx` - -**Features**: -- Full-screen modal with semi-transparent background -- Cancel and Send buttons in header -- Centered recording interface (TODO) -- Voice recording controls (TODO) - -### Shared Styles - -Common styles used across the Onyx layout components. - -**Location**: `src/onyx/styles.ts` - -**Features**: -- Consistent styling for modals -- Shared button and text styles -- Layout constants -- Typography integration - -## Usage - -The layout is used by importing the OnyxLayout component: - -```tsx -import { OnyxLayout } from "@/onyx/OnyxLayout" - -export default function App() { - return ( - - - - ) -} -``` - -## Styling - -The layout uses a consistent style system with: -- Semi-transparent black modals (rgba(0,0,0,0.85)) -- White text for active elements -- Gray (#666) text for inactive/disabled elements -- Standard font sizes (17px for text) -- Consistent padding and spacing -- Custom typography from the app's theme - -## TODO - -### Voice Input Implementation -The voice input modal is currently a placeholder. Future implementation needs: -1. Voice recording functionality -2. Recording visualization -3. Audio data handling -4. Recording controls (start/stop/pause) -5. Proper audio file format handling -6. Upload/sending mechanism - -### General Improvements -1. Add loading states for send operations -2. Add error handling for failed sends -3. Add haptic feedback -4. Add animations for button presses -5. Add sound effects for recording start/stop -6. Improve keyboard handling on different devices -7. Add accessibility features - -## Component Communication - -The components communicate through props and callbacks: - -```tsx -// Text input handling -const handleTextSend = (text: string) => { - // Handle sending text message -} - -// Voice input handling -const handleVoiceSend = (audioData: any) => { - // Handle sending voice message -} - - setShowTextInput(false)} - onSend={handleTextSend} -/> -``` - -## Modal States - -Both modals handle several states: -1. Hidden (default) -2. Visible/Active -3. Processing (when sending) -4. Error (when send fails) - -The visibility is controlled by the parent OnyxLayout component through the `visible` prop. \ No newline at end of file diff --git a/docs/permissions.md b/docs/permissions.md index 8f9fc94b..05765d7e 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -9,10 +9,12 @@ Onyx requires certain permissions to enable voice input features. The permission ## Required Permissions ### Android + - `RECORD_AUDIO` - Required for microphone access - Speech recognition services are handled through Google's services ### iOS + - Microphone permission - Handled automatically by Voice API - Speech recognition permission - Handled automatically by Voice API @@ -23,13 +25,14 @@ Onyx requires certain permissions to enable voice input features. The permission The app uses a custom hook `useVoicePermissions` to manage voice-related permissions: ```typescript -import { useVoicePermissions } from '../hooks/useVoicePermissions' +import { useVoicePermissions } from "../hooks/useVoicePermissions" // Usage in components const { hasPermission, isChecking, requestPermissions } = useVoicePermissions() ``` The hook provides: + - `hasPermission`: Boolean indicating if required permissions are granted - `isChecking`: Boolean indicating if permissions are being checked - `requestPermissions()`: Function to request necessary permissions @@ -38,21 +41,20 @@ The hook provides: ### Platform-Specific Handling #### Android + ```typescript // Using PermissionsAndroid API -const granted = await PermissionsAndroid.request( - PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, - { - title: "Microphone Permission", - message: "Onyx needs access to your microphone for voice input", - buttonNeutral: "Ask Me Later", - buttonNegative: "Cancel", - buttonPositive: "OK" - } -) +const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, { + title: "Microphone Permission", + message: "Onyx needs access to your microphone for voice input", + buttonNeutral: "Ask Me Later", + buttonNegative: "Cancel", + buttonPositive: "OK", +}) ``` #### iOS + - Permissions are handled automatically by the Voice API - No manual permission requests needed - System permission dialogs appear when needed @@ -60,6 +62,7 @@ const granted = await PermissionsAndroid.request( ## Voice Input Modal The VoiceInputModal component uses the permissions hook to: + 1. Check permissions when opened 2. Request permissions if needed 3. Show appropriate UI states: @@ -76,7 +79,7 @@ const VoiceInputModal = () => { startRecording() } }, [visible, hasPermission]) - + // ... rest of the component } ``` @@ -86,6 +89,7 @@ const VoiceInputModal = () => { ### Android Manifest Required permissions are declared in `android/app/src/main/AndroidManifest.xml`: + ```xml ``` @@ -93,6 +97,7 @@ Required permissions are declared in `android/app/src/main/AndroidManifest.xml`: ### iOS Info.plist Required permission descriptions in `ios/[AppName]/Info.plist`: + ```xml NSMicrophoneUsageDescription Onyx needs access to your microphone for voice input @@ -103,12 +108,14 @@ Required permission descriptions in `ios/[AppName]/Info.plist`: ## Error Handling The permission system handles several cases: + 1. Permission denied 2. Permission not yet requested 3. System errors 4. Missing speech recognition services (Android) Error messages are displayed to users with appropriate actions: + - Clear explanation of why permissions are needed - Option to retry permission request - Guidance for enabling permissions in settings @@ -116,16 +123,19 @@ Error messages are displayed to users with appropriate actions: ## Best Practices 1. **Request Timing** + - Only request permissions when needed - Check permissions before starting voice input - Provide clear context for permission requests 2. **User Experience** + - Show loading states during permission checks - Provide clear feedback for permission status - Gracefully handle permission denials 3. **Error Handling** + - Catch and handle all permission-related errors - Provide clear error messages - Guide users to resolve permission issues @@ -138,6 +148,7 @@ Error messages are displayed to users with appropriate actions: ## Testing Test the following scenarios: + 1. First-time permission requests 2. Permission denials 3. Permission grants @@ -149,11 +160,13 @@ Test the following scenarios: Common issues and solutions: 1. **Permission Denied** + - Guide users to app settings - Explain why permissions are needed - Provide retry option 2. **Android Speech Services** + - Check Google app installation - Verify Google services availability - Handle offline scenarios @@ -166,8 +179,9 @@ Common issues and solutions: ## Future Improvements Planned enhancements: + 1. Better permission denial handling 2. Offline voice recognition options 3. Enhanced error messages 4. Permission settings deep linking -5. Permission status persistence \ No newline at end of file +5. Permission status persistence diff --git a/docs/tool-component.md b/docs/tool-component.md deleted file mode 100644 index e153c40b..00000000 --- a/docs/tool-component.md +++ /dev/null @@ -1,182 +0,0 @@ -# Tool Invocation Component - -The Tool Invocation component displays the execution and results of AI tool calls in a mobile-friendly format. It's designed to show tool execution status, parameters, and results in a clear and interactive way. - -## Location - -- Main component: `app/onyx/markdown/ToolInvocation.tsx` -- Integration: `app/onyx/markdown/MessageContent.tsx` - -## Features - -- Display tool execution status (pending/completed/failed) -- Show tool name and repository information -- View input parameters in a modal -- View file contents in a modal (when available) -- Mobile-optimized touch interactions -- Native styling matching app theme - -## Component Structure - -### ToolInvocation Props - -```typescript -interface ToolInvocation { - id?: string - toolCallId?: string - tool_name?: string - toolName?: string - input?: JSONValue - args?: JSONValue - output?: JSONValue - result?: JSONValue - status?: 'pending' | 'completed' | 'failed' - state?: 'call' | 'result' | 'partial-call' -} - -interface ModalContentProps { - title: string - description: string - content: any - visible: boolean - onClose: () => void -} -``` - -### Visual Elements - -1. **Header** - - Tool name - - Repository info (if available) - - Status indicator (⟳ pending, ✓ completed, ✗ failed) - -2. **Content** - - Summary of tool execution - - Interactive buttons for viewing details - -3. **Modals** - - Input Parameters modal - - File Content modal (when available) - -## Usage in MessageContent - -The ToolInvocation component is integrated into the MessageContent component to display tool calls within chat messages: - -```typescript -interface MessageContentProps { - message: Message & { - toolInvocations?: Array - } -} - -// Usage in render - - {hasContent && ( - - {message.content} - - )} - - {hasToolInvocations && ( - - {message.toolInvocations.map((invocation, index) => ( - - ))} - - )} - -``` - -## Styling - -The component uses React Native's StyleSheet system and follows the app's theme colors: - -```typescript -const styles = StyleSheet.create({ - card: { - backgroundColor: colors.background, - borderRadius: 8, - marginBottom: 8, - borderWidth: 1, - borderColor: colors.border, - }, - // ... other styles -}) -``` - -Key style features: -- Consistent border radius and spacing -- Clear visual hierarchy -- Responsive touch targets -- Modal overlay for detailed information -- Status-specific colors for indicators -- Proper spacing when combined with message content - -## Modal System - -The component uses React Native's Modal component for displaying detailed information: - -1. **Input Parameters Modal** - - Shows all input parameters in a scrollable view - - JSON formatting for readability - - Close button for dismissal - - Semi-transparent overlay background - -2. **File Content Modal** - - Displays file contents when available - - Scrollable for long content - - Monospace font for code readability - - Maximum height constraint with scrolling - -## Status Indicators - -Status is displayed using text characters for simplicity and reliability: -- ⟳ Pending (gray color) -- ✓ Completed (text color) -- ✗ Failed (error color) - -## Integration with Message System - -The component integrates with the AI message system by: -1. Detecting tool invocations in messages -2. Rendering tool cards below message content -3. Maintaining message flow and readability -4. Preserving message hierarchy -5. Supporting partial call states during execution - -## Best Practices - -When using the ToolInvocation component: - -1. Always provide complete tool invocation data -2. Handle all possible states (pending/completed/failed/partial-call) -3. Ensure modals have appropriate content -4. Test touch interactions for usability -5. Verify status indicator visibility -6. Check modal scrolling with large content -7. Consider content spacing when tools appear with message text - -## Error Handling - -The component includes error handling for: -- Invalid tool invocation data -- JSON parsing errors -- Missing or malformed content -- Modal interaction edge cases -- Missing optional fields - -## Future Improvements - -Potential areas for enhancement: -1. Animation for status changes -2. Expanded touch interactions -3. Additional tool-specific displays -4. Enhanced error messaging -5. Accessibility improvements -6. Performance optimizations for large datasets -7. Custom styling per tool type -8. Interactive tool results -9. Retry functionality for failed invocations \ No newline at end of file diff --git a/docs/tools.md b/docs/tools.md deleted file mode 100644 index ebcfb7e5..00000000 --- a/docs/tools.md +++ /dev/null @@ -1,226 +0,0 @@ -# Tools System Documentation - -This document describes the tools system in Onyx, including the architecture, available tools, and how tools are managed between client and server. - -## Architecture - -The tools system consists of three main components: - -1. Server-side Tool Definitions (Nexus) -2. Client-side Tool Management (Onyx) -3. LLM Integration (Gemini) - -### Server-side Tool Definitions - -Located in `nexus/src/tools/`, the server manages tool implementations: - -```typescript -// Example from src/tools.ts -export const allTools = { - view_file: { tool: viewFileTool, description: "View file contents at path" }, - view_folder: { tool: viewFolderTool, description: "View folder contents at path" }, - create_file: { tool: createFileTool, description: "Create a new file at path with content" }, - rewrite_file: { tool: rewriteFileTool, description: "Rewrite file at path with new content" }, -} -``` - -Key features: -- Centralized tool definitions -- Standardized implementations -- Consistent error handling -- Server-side execution - -### Client-side Tool Management - -The client manages tool enablement through the ChatStore: - -```typescript -// From app/models/chat/ChatStore.ts -export const ChatStoreModel = types - .model("ChatStore") - .props({ - enabledTools: types.optional(types.array(types.string), [ - "view_file", - "view_folder", - "create_file", - "rewrite_file" - ]), - }) - .actions(self => ({ - toggleTool(toolName: string) { - const index = self.enabledTools.indexOf(toolName) - if (index === -1) { - self.enabledTools.push(toolName) - } else { - self.enabledTools.splice(index, 1) - } - }, - setEnabledTools(tools: string[]) { - self.enabledTools.replace(tools) - } - })) - .views(self => ({ - isToolEnabled(toolName: string) { - return self.enabledTools.includes(toolName) - } - })) -``` - -### LLM Integration - -Tools are integrated with Gemini through the Nexus server: -- Tool availability based on client selection -- Parameter validation -- Result processing -- Error handling - -## Available Tools - -### GitHub Tools - -#### 1. View File (`view_file`) -Views contents of a file in a GitHub repository. - -Parameters: -- `path` (string): File path -- `owner` (string): Repository owner -- `repo` (string): Repository name -- `branch` (string): Branch name - -#### 2. View Folder (`view_folder`) -Views the file/folder structure at a path. - -Parameters: -- `path` (string): Directory path -- `owner` (string): Repository owner -- `repo` (string): Repository name -- `branch` (string): Branch name - -#### 3. Create File (`create_file`) -Creates a new file with specified content. - -Parameters: -- `path` (string): File path -- `content` (string): File content -- `owner` (string): Repository owner -- `repo` (string): Repository name -- `branch` (string): Branch name - -#### 4. Rewrite File (`rewrite_file`) -Rewrites an existing file with new content. - -Parameters: -- `path` (string): File path -- `content` (string): New file content -- `owner` (string): Repository owner -- `repo` (string): Repository name -- `branch` (string): Branch name - -## Tool Management UI - -Tools are managed through the RepoSection component: - -```typescript -// From app/onyx/RepoSection.tsx -const AVAILABLE_TOOLS = [ - { id: "view_file", name: "View File", description: "View file contents at path" }, - { id: "view_folder", name: "View Folder", description: "View file/folder hierarchy at path" }, - { id: "create_file", name: "Create File", description: "Create a new file at path with content" }, - { id: "rewrite_file", name: "Rewrite File", description: "Rewrite file at path with new content" }, -] -``` - -Features: -- Individual tool enablement -- Tool descriptions -- Persistent tool state -- Visual feedback -- Mobile-friendly interface - -## Request Flow - -1. Client sends request with: - - Message content - - GitHub token - - Enabled tools list - - Active repositories - -2. Server processes request: - - Validates GitHub token - - Filters available tools based on enabled list - - Provides tools to Gemini - - Executes tool calls - - Returns results - -3. Client displays results: - - Tool invocation status - - Execution results - - Error messages if any - -## Best Practices - -1. Tool Design -- Clear descriptions -- Validated parameters -- Consistent error handling -- Comprehensive documentation - -2. Security -- Parameter validation -- Permission checking -- Rate limiting -- Audit logging - -3. Performance -- Efficient tool execution -- Result caching -- Resource cleanup - -## Error Handling - -Tools return standardized errors: - -```typescript -interface ToolError { - success: false - error: string - details?: unknown -} - -// Example -return { - success: false, - error: "Invalid parameters provided", - details: { param: "path", error: "Path cannot be empty" } -} -``` - -Common error codes: -- `INVALID_PARAMS`: Invalid parameters -- `NOT_FOUND`: Resource not found -- `PERMISSION_DENIED`: Insufficient permissions -- `RATE_LIMITED`: Rate limit exceeded -- `INTERNAL_ERROR`: Internal tool error - -## Future Improvements - -1. Tool Discovery -- Dynamic tool loading -- Tool categories -- Version management - -2. Advanced Features -- Tool composition -- Workflow automation -- Custom tool creation - -3. UI Enhancements -- Tool usage analytics -- Performance metrics -- Debug console - -## Resources - -- [Gemini API Documentation](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini) -- [GitHub API Documentation](https://docs.github.com/en/rest) -- [MST Documentation](https://mobx-state-tree.js.org/) \ No newline at end of file diff --git a/eas.json b/eas.json index 2031f3c3..53580eac 100644 --- a/eas.json +++ b/eas.json @@ -65,14 +65,14 @@ "ENVIRONMENT": "production" }, "environment": "production", - "channel": "v0.0.5" + "channel": "v0.0.6" }, "production-apk": { "extends": "production", "android": { "buildType": "apk" }, - "channel": "v0.0.5-apk" + "channel": "v0.0.6-apk" } }, "submit": { diff --git a/package.json b/package.json index 63fc05ae..39d5bef9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "onyx", "main": "app/app.tsx", - "version": "0.0.5", + "version": "0.0.6", "license": "AGPL-3.0-only", "scripts": { "start": "expo start", @@ -23,9 +23,9 @@ "doctor": "npx expo-doctor", "copyrepo": "cd ~/code && node md.js ./onyx && cat combined.md | pbcopy", "copy2": "cd ~/code && node md2.js ./onyx && cat model-system-docs.md | pbcopy", - "update:both": "eas update --channel v0.0.5 --environment production --message", - "update:ios": "eas update --channel v0.0.5 --environment production --platform ios --message", - "update:android": "eas update --channel v0.0.5 -environment production --platform android --message", + "update:both": "eas update --channel v0.0.6 --environment production --message", + "update:ios": "eas update --channel v0.0.6 --environment production --platform ios --message", + "update:android": "eas update --channel v0.0.6 -environment production --platform android --message", "hierarchy": "node scripts/generate-hierarchy.js" }, "jest": { @@ -62,6 +62,7 @@ "expo-gl": "~15.0.2", "expo-haptics": "~14.0.0", "expo-linking": "~7.0.3", + "expo-localization": "~16.0.0", "expo-secure-store": "~14.0.0", "expo-speech": "~13.0.0", "expo-splash-screen": "~0.29.18", @@ -71,6 +72,8 @@ "expo-system-ui": "~4.0.6", "expo-updates": "~0.26.10", "expo-web-browser": "~14.0.1", + "i18next": "24.2.0", + "intl-pluralrules": "2.0.1", "mobx": "6.10.2", "mobx-react-lite": "4.0.5", "mobx-state-tree": "5.3.0", @@ -79,10 +82,12 @@ "postinstall-postinstall": "^2.1.0", "react": "18.3.1", "react-dom": "18.3.1", + "react-i18next": "15.2.0", "react-native": "0.76.5", "react-native-document-picker": "^9.3.1", "react-native-drawer-layout": "^4.1.1", "react-native-gesture-handler": "~2.20.2", + "react-native-keyboard-controller": "1.15.2", "react-native-markdown-display": "^7.0.2", "react-native-mmkv": "^3.2.0", "react-native-reanimated": "~3.16.1", diff --git a/scripts/generate-hierarchy.js b/scripts/generate-hierarchy.js index f0aa7680..8e4c23d5 100644 --- a/scripts/generate-hierarchy.js +++ b/scripts/generate-hierarchy.js @@ -92,6 +92,9 @@ function generateHierarchy() { ignore: [ "node_modules/**", ".git/**", + "assets/icons/**", + "assets/images/**", + "app/i18n/**", ], }); diff --git a/tsconfig.json b/tsconfig.json index f65756de..46beb3cb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,9 @@ }, "module": "esnext", "moduleResolution": "node", - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "noImplicitAny": false }, - "include": ["**/*.ts", "**/*.tsx"] -} \ No newline at end of file + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["app/old/**"] +} diff --git a/yarn.lock b/yarn.lock index 785e1c63..54372aba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -804,7 +804,7 @@ pirates "^4.0.6" source-map-support "^0.5.16" -"@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.25.0", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0", "@babel/runtime@^7.8.4": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== @@ -3576,6 +3576,13 @@ expo-linking@~7.0.3: expo-constants "~17.0.0" invariant "^2.2.4" +expo-localization@~16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/expo-localization/-/expo-localization-16.0.0.tgz#fe426550649b6a8ea26876f26c065e5dcf6d5bb9" + integrity sha512-PaWDUs6sNaEbFwQc6QKsTfYCg9GDo3bBl+cWnoG0G7pn1A623CcMwWyV7jD5jpqz0s1gHmwSRjR3vKOqhouRWg== + dependencies: + rtl-detect "^1.0.2" + expo-manifests@~0.15.0: version "0.15.4" resolved "https://registry.yarnpkg.com/expo-manifests/-/expo-manifests-0.15.4.tgz#43627d00c5ef8163ffd1880e42093d342bc7e80f" @@ -4187,6 +4194,13 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + http-errors@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" @@ -4225,6 +4239,13 @@ hyphenate-style-name@^1.0.3: resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz#1797bf50369588b47b72ca6d5e65374607cf4436" integrity sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw== +i18next@24.2.0: + version "24.2.0" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-24.2.0.tgz#344c15a6f3b43d02aed78e2b08532e5b170951b4" + integrity sha512-ArJJTS1lV6lgKH7yEf4EpgNZ7+THl7bsGxxougPYiXRTJ/Fe1j08/TBpV9QsXCIYVfdE/HWG/xLezJ5DOlfBOA== + dependencies: + "@babel/runtime" "^7.23.2" + iconv-lite@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -4314,6 +4335,11 @@ internal-ip@^4.3.0: default-gateway "^4.2.0" ipaddr.js "^1.9.0" +intl-pluralrules@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/intl-pluralrules/-/intl-pluralrules-2.0.1.tgz#de16c3df1e09437635829725e88ea70c9ad79569" + integrity sha512-astxTLzIdXPeN0K9Rumi6LfMpm3rvNO0iJE+h/k8Kr/is+wPbRe4ikyDjlLr6VTh/mEfNv8RjN+gu3KwDiuhqg== + invariant@2.2.4, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -6367,6 +6393,14 @@ react-freeze@^1.0.0: resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.4.tgz#cbbea2762b0368b05cbe407ddc9d518c57c6f3ad" integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA== +react-i18next@15.2.0: + version "15.2.0" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.2.0.tgz#6b51650e1e93eb4d235a4d533fcf61b3bbf4ea10" + integrity sha512-iJNc8111EaDtVTVMKigvBtPHyrJV+KblWG73cUxqp+WmJCcwkzhWNFXmkAD5pwP2Z4woeDj/oXDdbjDsb3Gutg== + dependencies: + "@babel/runtime" "^7.25.0" + html-parse-stringify "^3.0.1" + "react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.2.0, react-is@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" @@ -6413,6 +6447,18 @@ react-native-image-viewing@^0.2.1: resolved "https://registry.yarnpkg.com/react-native-image-viewing/-/react-native-image-viewing-0.2.2.tgz#fb26e57d7d3d9ce4559a3af3d244387c0367242b" integrity sha512-osWieG+p/d2NPbAyonOMubttajtYEYiRGQaJA54slFxZ69j1V4/dCmcrVQry47ktVKy8/qpFwCpW1eT6MH5T2Q== +react-native-is-edge-to-edge@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.6.tgz#69ec13f70d76e9245e275eed4140d0873a78f902" + integrity sha512-1pHnFTlBahins6UAajXUqeCOHew9l9C2C8tErnpGC3IyLJzvxD+TpYAixnCbrVS52f7+NvMttbiSI290XfwN0w== + +react-native-keyboard-controller@1.15.2: + version "1.15.2" + resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.15.2.tgz#9c675053dccd6f7c9a17e9762f617d296d1ab999" + integrity sha512-ZN151OyMJ2GQkhebARY/5G9rXgSlNCKy+WjS6p4o7S+5ulb4nGzl6UkpEuT7/C6bHDeAjDupdrET9tyyTee3nA== + dependencies: + react-native-is-edge-to-edge "^1.1.6" + react-native-markdown-display@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/react-native-markdown-display/-/react-native-markdown-display-7.0.2.tgz#b6584cec8d6670c0141fb8780bc2f0710188a4c2" @@ -6802,6 +6848,11 @@ rimraf@~2.6.2: dependencies: glob "^7.1.3" +rtl-detect@^1.0.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.1.2.tgz#ca7f0330af5c6bb626c15675c642ba85ad6273c6" + integrity sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -7755,6 +7806,11 @@ vlq@^1.0.0: resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468" integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w== +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + w3c-xmlserializer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073"