diff --git a/README.ja.md b/README.ja.md index 55c8851..5a42a1b 100644 --- a/README.ja.md +++ b/README.ja.md @@ -133,12 +133,21 @@ bun reset-project # クリーン状態にリセット ### APIキーの設定 +Jules APIキーは2つの方法で設定できます: + +**方法1:環境変数(開発時に推奨)** +ルートディレクトリに`.env`ファイルを作成し、キーを追加します: +```bash +JULES_API_KEY=あなたの_api_key +``` + +**方法2:アプリ内設定** 1. アプリを開く 2. **Settings**タブに移動 3. Jules APIキーを入力 4. キーはデバイスにセキュアに保存されます -> APIキーは[Google Cloud Console](https://console.cloud.google.com/)またはJulesの設定ページから取得できます。 +> 💡 APIキーは[Google Cloud Console](https://console.cloud.google.com/)またはJulesの設定ページから取得できます。 ## プロジェクト構成 diff --git a/README.md b/README.md index b48714a..474f67d 100644 --- a/README.md +++ b/README.md @@ -154,8 +154,17 @@ bun reset-project # Reset to clean state ### API Key Setup +You can configure your Jules API key in two ways: + +**Method 1: Environment Variable (Recommended for development)** +Create a `.env` file in the root directory and add your key: +```bash +JULES_API_KEY=your_api_key_here +``` + +**Method 2: In-App Settings** 1. Open the app -2. Navigate to **Settings** tab +2. Navigate to the **Settings** tab 3. Enter your Jules API Key 4. The key is securely stored on your device diff --git a/app/create-session.tsx b/app/create-session.tsx index 02f460a..87cf974 100644 --- a/app/create-session.tsx +++ b/app/create-session.tsx @@ -1,23 +1,16 @@ -import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { View, Text, - TextInput, TouchableOpacity, - StyleSheet, ScrollView, Alert, - ActivityIndicator, KeyboardAvoidingView, Platform, - Animated, } from 'react-native'; import { router, Stack } from 'expo-router'; -import { LinearGradient } from 'expo-linear-gradient'; -import * as Haptics from 'expo-haptics'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useHeaderHeight } from '@react-navigation/elements'; -import { IconSymbol } from '@/components/ui/icon-symbol'; import { useJulesApi } from '@/hooks/use-jules-api'; import { useColorScheme } from '@/hooks/use-color-scheme'; import { useI18n } from '@/constants/i18n-context'; @@ -26,443 +19,13 @@ import { useSecureStorage } from '@/hooks/use-secure-storage'; import { useSourcesCache } from '@/hooks/use-sources-cache'; import type { Source } from '@/constants/types'; import { Colors } from '@/constants/theme'; -import { Skeleton } from '@/components/ui/skeleton'; -/** - * フォームスケルトン - */ -function FormSkeleton({ paddingBottom }: { paddingBottom: number }) { - const colorScheme = useColorScheme(); - const isDark = colorScheme === 'dark'; - - return ( - - {/* ラベル1 */} - - - {/* セレクトボックス */} - - - - - - - {/* ラベル2 */} - - - {/* テキストエリア */} - - - - - - - - {/* ボタン */} - - - ); -} - -const skeletonStyles = StyleSheet.create({ - content: { - padding: 16, - }, - section: { - gap: 8, - }, - selectBox: { - backgroundColor: '#ffffff', - borderWidth: 1, - borderColor: '#e2e8f0', - borderRadius: 12, - padding: 14, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - selectBoxDark: { - backgroundColor: '#1e293b', - borderColor: '#334155', - }, - textArea: { - backgroundColor: '#ffffff', - borderWidth: 1, - borderColor: '#e2e8f0', - borderRadius: 12, - padding: 14, - height: 120, - }, - textAreaDark: { - backgroundColor: '#1e293b', - borderColor: '#334155', - }, -}); - -function SourceSelector({ - isDark, - t, - isLoading, - selectedSource, - sourcesMap, - getSourceDisplayName, - toggleSources, - isDropdownOpen, - sourcesLoaded, - sources, - sourceQuery, - setSourceQuery, - handleSourcesScroll, - filteredRecentRepos, - filteredAllSources, - setSelectedSource, - setIsDropdownOpen, - isLoadingMoreSources, - hasMoreSources, -}: any) { - return ( - - - {t('selectRepo')} - - - - {selectedSource - ? (() => { - const source = sourcesMap.get(selectedSource); - return source ? getSourceDisplayName(source) : selectedSource; - })() - : t('selectPlaceholder')} - - - - - {/* Source list with lazy loading */} - {isDropdownOpen && sourcesLoaded && sources.length > 0 && ( - - - - - - - - {/* Recent Repositories Section */} - {filteredRecentRepos.length > 0 && ( - <> - - - - {t('recentRepos')} - - - {filteredRecentRepos.map((source: any) => { - return ( - { - setSelectedSource(source.name); - setIsDropdownOpen(false); - setSourceQuery(''); - }} - > - - - {getSourceDisplayName(source)} - - - ); - })} - - )} - - {/* All Repositories Section */} - {filteredAllSources.length > 0 && ( - <> - {filteredRecentRepos.length > 0 && ( - - - - {t('allRepos')} - - - )} - {filteredAllSources.map((source: any) => { - return ( - { - setSelectedSource(source.name); - setIsDropdownOpen(false); - setSourceQuery(''); - }} - > - - - {getSourceDisplayName(source)} - - - ); - })} - - )} - {/* Loading indicator for more sources */} - {isLoadingMoreSources && ( - - - - {t('loadingMore')} - - - )} - {/* End of list indicator */} - {!hasMoreSources && sources.length > 20 && ( - - - {sources.length} repos - - - )} - - - )} - - {sourcesLoaded && isDropdownOpen && (sources.length === 0 || (filteredRecentRepos.length === 0 && filteredAllSources.length === 0 && !isLoadingMoreSources)) && ( - - {t('noSourcesFound')} - - )} - - {/* Helper hint */} - {!isDropdownOpen && sourcesLoaded && sources.length > 0 && ( - - {t('repoHint')} - - )} - - ); -} - -function PromptInput({ - isDark, - t, - prompt, - setPrompt, -}: any) { - return ( - - - {t('promptLabel')} - - {prompt.length} chars - - - - - ); -} - -function ExecutionModeSelector({ - isDark, - t, - requirePlanApproval, - setRequirePlanApproval, -}: any) { - return ( - - {t('executionMode')} - - {/* Start Mode */} - setRequirePlanApproval(false)} - activeOpacity={0.7} - > - - - - {t('modeStart')} - - - - {t('modeStartDesc')} - - - - {/* Review Mode */} - setRequirePlanApproval(true)} - activeOpacity={0.7} - > - - - - {t('modeReview')} - - - - {t('modeReviewDesc')} - - - - - ); -} - -function SubmitButton({ - selectedSource, - prompt, - isLoading, - handleCreate, - t, - colors, -}: any) { - const buttonLabel = isLoading ? 'Creating...' : 'Start Task'; - - return ( - { - void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); - void handleCreate(); - }} - disabled={!selectedSource || !prompt.trim() || isLoading} - activeOpacity={0.9} - > - {(!selectedSource || !prompt.trim()) ? ( - - - - {buttonLabel} - - - ) : ( - - {isLoading ? ( - - ) : ( - - )} - {buttonLabel} - - )} - - ); -} +import { FormSkeleton } from '@/components/jules/create-session/form-skeleton'; +import { SourceSelector } from '@/components/jules/create-session/source-selector'; +import { PromptInput } from '@/components/jules/create-session/prompt-input'; +import { ExecutionModeSelector } from '@/components/jules/create-session/execution-mode-selector'; +import { SubmitButton } from '@/components/jules/create-session/submit-button'; +import { styles } from '@/components/jules/create-session/styles'; export default function CreateSessionScreen() { const colorScheme = useColorScheme(); @@ -582,7 +145,6 @@ export default function CreateSessionScreen() { : source.displayName || source.name; }, []); - // Toggle dropdown (sources already loaded) const toggleSources = useCallback(() => { if (isDropdownOpen) { @@ -722,307 +284,3 @@ export default function CreateSessionScreen() { ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f8fafc', - }, - containerDark: { - backgroundColor: '#020617', - }, - content: { - padding: 16, - }, - errorBanner: { - marginBottom: 16, - padding: 12, - backgroundColor: 'rgba(239, 68, 68, 0.1)', - borderRadius: 8, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - errorBannerDark: { - backgroundColor: 'rgba(239, 68, 68, 0.2)', - }, - errorText: { - color: '#dc2626', - fontSize: 13, - flex: 1, - }, - errorClose: { - color: '#dc2626', - fontSize: 18, - fontWeight: '700', - paddingLeft: 12, - }, - section: { - gap: 8, - }, - label: { - fontSize: 14, - fontWeight: '600', - color: '#334155', - }, - labelDark: { - color: '#cbd5e1', - }, - labelRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 8, - }, - charCounter: { - fontSize: 12, - color: '#94a3b8', - }, - charCounterDark: { - color: '#64748b', - }, - selectButton: { - backgroundColor: '#ffffff', - borderWidth: 1, - borderColor: '#e2e8f0', - borderRadius: 12, - padding: 14, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - selectButtonDark: { - backgroundColor: '#1e293b', - borderColor: '#334155', - }, - selectButtonText: { - fontSize: 15, - color: '#0f172a', - flex: 1, - }, - selectButtonTextDark: { - color: '#f8fafc', - }, - placeholderText: { - color: '#94a3b8', - }, - sourceList: { - backgroundColor: '#ffffff', - borderWidth: 1, - borderColor: '#e2e8f0', - borderRadius: 12, - marginTop: 8, - overflow: 'hidden', - maxHeight: 300, - }, - repoSearchContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - paddingHorizontal: 12, - paddingVertical: 10, - borderBottomWidth: 1, - borderBottomColor: '#e2e8f0', - backgroundColor: '#f8fafc', - }, - repoSearchContainerDark: { - borderBottomColor: '#334155', - backgroundColor: '#0f172a', - }, - repoSearchInput: { - flex: 1, - fontSize: 13, - color: '#334155', - paddingVertical: 0, - }, - repoSearchInputDark: { - color: '#cbd5e1', - }, - sourceListDark: { - backgroundColor: '#1e293b', - borderColor: '#334155', - }, - sourceItem: { - flexDirection: 'row', - alignItems: 'center', - gap: 10, - padding: 12, - borderBottomWidth: 1, - borderBottomColor: '#f1f5f9', - }, - sourceItemDark: { - borderBottomColor: '#334155', - }, - sourceItemSelected: { - backgroundColor: 'rgba(37, 99, 235, 0.1)', - }, - sourceItemText: { - fontSize: 14, - color: '#334155', - flex: 1, - }, - sourceItemTextDark: { - color: '#e2e8f0', - }, - sourceItemTextSelected: { - color: '#2563eb', - fontWeight: '600', - }, - hint: { - fontSize: 12, - color: '#94a3b8', - marginTop: 4, - }, - hintDark: { - color: '#64748b', - }, - textArea: { - backgroundColor: '#ffffff', - borderWidth: 1, - borderColor: '#e2e8f0', - borderRadius: 12, - padding: 14, - fontSize: 15, - color: '#0f172a', - height: 120, - }, - textAreaDark: { - backgroundColor: '#1e293b', - borderColor: '#334155', - color: '#f8fafc', - }, - createButton: { - borderRadius: 14, - marginTop: 24, - shadowColor: '#6366f1', - shadowOffset: { width: 0, height: 6 }, - shadowOpacity: 0.3, - shadowRadius: 12, - elevation: 6, - overflow: 'hidden', - }, - createButtonContent: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: 10, - paddingVertical: 16, - backgroundColor: '#e2e8f0', - }, - createButtonGradient: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: 10, - paddingVertical: 16, - }, - createButtonDisabled: { - shadowOpacity: 0, - }, - createButtonText: { - color: '#ffffff', - fontSize: 17, - fontWeight: '700', - letterSpacing: 0.3, - }, - createButtonTextDisabled: { - color: '#94a3b8', - }, - loadingMore: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - padding: 12, - gap: 8, - }, - loadingMoreText: { - fontSize: 12, - color: '#64748b', - }, - loadingMoreTextDark: { - color: '#94a3b8', - }, - endOfList: { - alignItems: 'center', - padding: 8, - }, - endOfListText: { - fontSize: 11, - color: '#94a3b8', - }, - endOfListTextDark: { - color: '#64748b', - }, - sectionHeader: { - flexDirection: 'row', - alignItems: 'center', - gap: 6, - paddingHorizontal: 12, - paddingVertical: 8, - backgroundColor: '#f8fafc', - borderBottomWidth: 1, - borderBottomColor: '#e2e8f0', - }, - sectionHeaderDark: { - backgroundColor: '#0f172a', - borderBottomColor: '#334155', - }, - sectionHeaderText: { - fontSize: 12, - fontWeight: '600', - color: '#64748b', - textTransform: 'uppercase', - letterSpacing: 0.5, - }, - sectionHeaderTextDark: { - color: '#94a3b8', - }, - modeContainer: { - flexDirection: 'row', - gap: 12, - }, - modeButton: { - flex: 1, - backgroundColor: '#ffffff', - borderWidth: 2, - borderColor: '#e2e8f0', - borderRadius: 12, - padding: 14, - gap: 8, - }, - modeButtonDark: { - backgroundColor: '#1e293b', - borderColor: '#334155', - }, - modeButtonSelected: { - borderColor: '#2563eb', - backgroundColor: 'rgba(37, 99, 235, 0.05)', - }, - modeHeader: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - modeTitle: { - fontSize: 15, - fontWeight: '600', - color: '#334155', - }, - modeTitleDark: { - color: '#cbd5e1', - }, - modeTitleSelected: { - color: '#2563eb', - }, - modeDesc: { - fontSize: 12, - color: '#64748b', - lineHeight: 16, - }, - modeDescDark: { - color: '#94a3b8', - }, - modeDescSelected: { - color: '#3b82f6', - }, -}); diff --git a/components/jules/create-session/execution-mode-selector.tsx b/components/jules/create-session/execution-mode-selector.tsx new file mode 100644 index 0000000..b54863f --- /dev/null +++ b/components/jules/create-session/execution-mode-selector.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { View, Text, TouchableOpacity } from 'react-native'; +import { IconSymbol } from '@/components/ui/icon-symbol'; +import { styles } from './styles'; + +interface ExecutionModeSelectorProps { + isDark: boolean; + t: (key: any) => string; + requirePlanApproval: boolean; + setRequirePlanApproval: (requirePlanApproval: boolean) => void; +} + +export function ExecutionModeSelector({ + isDark, + t, + requirePlanApproval, + setRequirePlanApproval, +}: ExecutionModeSelectorProps) { + return ( + + {t('executionMode')} + + {/* Start Mode */} + setRequirePlanApproval(false)} + activeOpacity={0.7} + > + + + + {t('modeStart')} + + + + {t('modeStartDesc')} + + + + {/* Review Mode */} + setRequirePlanApproval(true)} + activeOpacity={0.7} + > + + + + {t('modeReview')} + + + + {t('modeReviewDesc')} + + + + + ); +} diff --git a/components/jules/create-session/form-skeleton.tsx b/components/jules/create-session/form-skeleton.tsx new file mode 100644 index 0000000..c2acd41 --- /dev/null +++ b/components/jules/create-session/form-skeleton.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { View, ScrollView, StyleSheet } from 'react-native'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useColorScheme } from '@/hooks/use-color-scheme'; + +/** + * フォームスケルトン + */ +export function FormSkeleton({ paddingBottom }: { paddingBottom: number }) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === 'dark'; + + return ( + + {/* ラベル1 */} + + + {/* セレクトボックス */} + + + + + + + {/* ラベル2 */} + + + {/* テキストエリア */} + + + + + + + + {/* ボタン */} + + + ); +} + +const skeletonStyles = StyleSheet.create({ + content: { + padding: 16, + }, + section: { + gap: 8, + }, + selectBox: { + backgroundColor: '#ffffff', + borderWidth: 1, + borderColor: '#e2e8f0', + borderRadius: 12, + padding: 14, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + selectBoxDark: { + backgroundColor: '#1e293b', + borderColor: '#334155', + }, + textArea: { + backgroundColor: '#ffffff', + borderWidth: 1, + borderColor: '#e2e8f0', + borderRadius: 12, + padding: 14, + height: 120, + }, + textAreaDark: { + backgroundColor: '#1e293b', + borderColor: '#334155', + }, +}); diff --git a/components/jules/create-session/prompt-input.tsx b/components/jules/create-session/prompt-input.tsx new file mode 100644 index 0000000..b320dd9 --- /dev/null +++ b/components/jules/create-session/prompt-input.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { View, Text, TextInput } from 'react-native'; +import { styles } from './styles'; + +interface PromptInputProps { + isDark: boolean; + t: (key: any) => string; + prompt: string; + setPrompt: (prompt: string) => void; +} + +export function PromptInput({ + isDark, + t, + prompt, + setPrompt, +}: PromptInputProps) { + return ( + + + {t('promptLabel')} + + {prompt.length} chars + + + + + ); +} diff --git a/components/jules/create-session/source-selector.tsx b/components/jules/create-session/source-selector.tsx new file mode 100644 index 0000000..68b2bb7 --- /dev/null +++ b/components/jules/create-session/source-selector.tsx @@ -0,0 +1,233 @@ +import React from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, + ActivityIndicator, +} from 'react-native'; +import { IconSymbol } from '@/components/ui/icon-symbol'; +import { styles } from './styles'; +import type { Source } from '@/constants/types'; + +interface SourceSelectorProps { + isDark: boolean; + t: (key: any) => string; + isLoading: boolean; + selectedSource: string; + sourcesMap: Map; + getSourceDisplayName: (source: Source) => string; + toggleSources: () => void; + isDropdownOpen: boolean; + sourcesLoaded: boolean; + sources: Source[]; + sourceQuery: string; + setSourceQuery: (query: string) => void; + handleSourcesScroll: (event: any) => void; + filteredRecentRepos: Source[]; + filteredAllSources: Source[]; + setSelectedSource: (source: string) => void; + setIsDropdownOpen: (isOpen: boolean) => void; + isLoadingMoreSources: boolean; + hasMoreSources: boolean; +} + +export function SourceSelector({ + isDark, + t, + isLoading, + selectedSource, + sourcesMap, + getSourceDisplayName, + toggleSources, + isDropdownOpen, + sourcesLoaded, + sources, + sourceQuery, + setSourceQuery, + handleSourcesScroll, + filteredRecentRepos, + filteredAllSources, + setSelectedSource, + setIsDropdownOpen, + isLoadingMoreSources, + hasMoreSources, +}: SourceSelectorProps) { + return ( + + + {t('selectRepo')} + + + + {selectedSource + ? (() => { + const source = sourcesMap.get(selectedSource); + return source ? getSourceDisplayName(source) : selectedSource; + })() + : t('selectPlaceholder')} + + + + + {/* Source list with lazy loading */} + {isDropdownOpen && sourcesLoaded && sources.length > 0 && ( + + + + + + + + {/* Recent Repositories Section */} + {filteredRecentRepos.length > 0 && ( + <> + + + + {t('recentRepos')} + + + {filteredRecentRepos.map((source: Source) => { + return ( + { + setSelectedSource(source.name); + setIsDropdownOpen(false); + setSourceQuery(''); + }} + > + + + {getSourceDisplayName(source)} + + + ); + })} + + )} + + {/* All Repositories Section */} + {filteredAllSources.length > 0 && ( + <> + {filteredRecentRepos.length > 0 && ( + + + + {t('allRepos')} + + + )} + {filteredAllSources.map((source: Source) => { + return ( + { + setSelectedSource(source.name); + setIsDropdownOpen(false); + setSourceQuery(''); + }} + > + + + {getSourceDisplayName(source)} + + + ); + })} + + )} + {/* Loading indicator for more sources */} + {isLoadingMoreSources && ( + + + + {t('loadingMore')} + + + )} + {/* End of list indicator */} + {!hasMoreSources && sources.length > 20 && ( + + + {sources.length} repos + + + )} + + + )} + + {sourcesLoaded && isDropdownOpen && (sources.length === 0 || (filteredRecentRepos.length === 0 && filteredAllSources.length === 0 && !isLoadingMoreSources)) && ( + + {t('noSourcesFound')} + + )} + + {/* Helper hint */} + {!isDropdownOpen && sourcesLoaded && sources.length > 0 && ( + + {t('repoHint')} + + )} + + ); +} diff --git a/components/jules/create-session/styles.ts b/components/jules/create-session/styles.ts new file mode 100644 index 0000000..e1b2755 --- /dev/null +++ b/components/jules/create-session/styles.ts @@ -0,0 +1,305 @@ +import { StyleSheet } from 'react-native'; + +export const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f8fafc', + }, + containerDark: { + backgroundColor: '#020617', + }, + content: { + padding: 16, + }, + errorBanner: { + marginBottom: 16, + padding: 12, + backgroundColor: 'rgba(239, 68, 68, 0.1)', + borderRadius: 8, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + errorBannerDark: { + backgroundColor: 'rgba(239, 68, 68, 0.2)', + }, + errorText: { + color: '#dc2626', + fontSize: 13, + flex: 1, + }, + errorClose: { + color: '#dc2626', + fontSize: 18, + fontWeight: '700', + paddingLeft: 12, + }, + section: { + gap: 8, + }, + label: { + fontSize: 14, + fontWeight: '600', + color: '#334155', + }, + labelDark: { + color: '#cbd5e1', + }, + labelRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + charCounter: { + fontSize: 12, + color: '#94a3b8', + }, + charCounterDark: { + color: '#64748b', + }, + selectButton: { + backgroundColor: '#ffffff', + borderWidth: 1, + borderColor: '#e2e8f0', + borderRadius: 12, + padding: 14, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + selectButtonDark: { + backgroundColor: '#1e293b', + borderColor: '#334155', + }, + selectButtonText: { + fontSize: 15, + color: '#0f172a', + flex: 1, + }, + selectButtonTextDark: { + color: '#f8fafc', + }, + placeholderText: { + color: '#94a3b8', + }, + sourceList: { + backgroundColor: '#ffffff', + borderWidth: 1, + borderColor: '#e2e8f0', + borderRadius: 12, + marginTop: 8, + overflow: 'hidden', + maxHeight: 300, + }, + repoSearchContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + paddingHorizontal: 12, + paddingVertical: 10, + borderBottomWidth: 1, + borderBottomColor: '#e2e8f0', + backgroundColor: '#f8fafc', + }, + repoSearchContainerDark: { + borderBottomColor: '#334155', + backgroundColor: '#0f172a', + }, + repoSearchInput: { + flex: 1, + fontSize: 13, + color: '#334155', + paddingVertical: 0, + }, + repoSearchInputDark: { + color: '#cbd5e1', + }, + sourceListDark: { + backgroundColor: '#1e293b', + borderColor: '#334155', + }, + sourceItem: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, + padding: 12, + borderBottomWidth: 1, + borderBottomColor: '#f1f5f9', + }, + sourceItemDark: { + borderBottomColor: '#334155', + }, + sourceItemSelected: { + backgroundColor: 'rgba(37, 99, 235, 0.1)', + }, + sourceItemText: { + fontSize: 14, + color: '#334155', + flex: 1, + }, + sourceItemTextDark: { + color: '#e2e8f0', + }, + sourceItemTextSelected: { + color: '#2563eb', + fontWeight: '600', + }, + hint: { + fontSize: 12, + color: '#94a3b8', + marginTop: 4, + }, + hintDark: { + color: '#64748b', + }, + textArea: { + backgroundColor: '#ffffff', + borderWidth: 1, + borderColor: '#e2e8f0', + borderRadius: 12, + padding: 14, + fontSize: 15, + color: '#0f172a', + height: 120, + }, + textAreaDark: { + backgroundColor: '#1e293b', + borderColor: '#334155', + color: '#f8fafc', + }, + createButton: { + borderRadius: 14, + marginTop: 24, + shadowColor: '#6366f1', + shadowOffset: { width: 0, height: 6 }, + shadowOpacity: 0.3, + shadowRadius: 12, + elevation: 6, + overflow: 'hidden', + }, + createButtonContent: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 10, + paddingVertical: 16, + backgroundColor: '#e2e8f0', + }, + createButtonGradient: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 10, + paddingVertical: 16, + }, + createButtonDisabled: { + shadowOpacity: 0, + }, + createButtonText: { + color: '#ffffff', + fontSize: 17, + fontWeight: '700', + letterSpacing: 0.3, + }, + createButtonTextDisabled: { + color: '#94a3b8', + }, + loadingMore: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + padding: 12, + gap: 8, + }, + loadingMoreText: { + fontSize: 12, + color: '#64748b', + }, + loadingMoreTextDark: { + color: '#94a3b8', + }, + endOfList: { + alignItems: 'center', + padding: 8, + }, + endOfListText: { + fontSize: 11, + color: '#94a3b8', + }, + endOfListTextDark: { + color: '#64748b', + }, + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 12, + paddingVertical: 8, + backgroundColor: '#f8fafc', + borderBottomWidth: 1, + borderBottomColor: '#e2e8f0', + }, + sectionHeaderDark: { + backgroundColor: '#0f172a', + borderBottomColor: '#334155', + }, + sectionHeaderText: { + fontSize: 12, + fontWeight: '600', + color: '#64748b', + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + sectionHeaderTextDark: { + color: '#94a3b8', + }, + modeContainer: { + flexDirection: 'row', + gap: 12, + }, + modeButton: { + flex: 1, + backgroundColor: '#ffffff', + borderWidth: 2, + borderColor: '#e2e8f0', + borderRadius: 12, + padding: 14, + gap: 8, + }, + modeButtonDark: { + backgroundColor: '#1e293b', + borderColor: '#334155', + }, + modeButtonSelected: { + borderColor: '#2563eb', + backgroundColor: 'rgba(37, 99, 235, 0.05)', + }, + modeHeader: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + modeTitle: { + fontSize: 15, + fontWeight: '600', + color: '#334155', + }, + modeTitleDark: { + color: '#cbd5e1', + }, + modeTitleSelected: { + color: '#2563eb', + }, + modeDesc: { + fontSize: 12, + color: '#64748b', + lineHeight: 16, + }, + modeDescDark: { + color: '#94a3b8', + }, + modeDescSelected: { + color: '#3b82f6', + }, +}); diff --git a/components/jules/create-session/submit-button.tsx b/components/jules/create-session/submit-button.tsx new file mode 100644 index 0000000..86ce8ee --- /dev/null +++ b/components/jules/create-session/submit-button.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { View, Text, TouchableOpacity, ActivityIndicator } from 'react-native'; +import { LinearGradient } from 'expo-linear-gradient'; +import * as Haptics from 'expo-haptics'; +import { IconSymbol } from '@/components/ui/icon-symbol'; +import { styles } from './styles'; + +interface SubmitButtonProps { + selectedSource: string; + prompt: string; + isLoading: boolean; + handleCreate: () => void; + t: (key: any) => string; + colors: any; +} + +export function SubmitButton({ + selectedSource, + prompt, + isLoading, + handleCreate, + t, + colors, +}: SubmitButtonProps) { + const buttonLabel = isLoading ? 'Creating...' : 'Start Task'; + + return ( + { + void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + void handleCreate(); + }} + disabled={!selectedSource || !prompt.trim() || isLoading} + activeOpacity={0.9} + > + {(!selectedSource || !prompt.trim()) ? ( + + + + {buttonLabel} + + + ) : ( + + {isLoading ? ( + + ) : ( + + )} + {buttonLabel} + + )} + + ); +} diff --git a/constants/api-key-context.tsx b/constants/api-key-context.tsx index b178929..6cad5b1 100644 --- a/constants/api-key-context.tsx +++ b/constants/api-key-context.tsx @@ -16,7 +16,7 @@ interface ApiKeyProviderProps { } export function ApiKeyProvider({ children }: ApiKeyProviderProps) { - const [apiKey, setApiKeyState] = useState(''); + const [apiKey, setApiKeyState] = useState(process.env.JULES_API_KEY || process.env.EXPO_PUBLIC_JULES_API_KEY || ''); const [isLoaded, setIsLoaded] = useState(false); // Load API key on mount diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 8ffc13e..00fcabc 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -39,6 +39,15 @@ bun web # Web browser ### 3. Configure API Key +You can configure your API key using an environment variable or via the app's settings UI. + +**Method 1: Environment Variable (Recommended)** +Create a `.env` file in the root directory: +```bash +JULES_API_KEY=your_api_key_here +``` + +**Method 2: In-App Settings** 1. Open the app on your device/simulator 2. Go to Settings tab 3. Enter your Jules API Key diff --git a/output.log b/output.log deleted file mode 100644 index b7183b6..0000000 --- a/output.log +++ /dev/null @@ -1,97 +0,0 @@ -Starting project at /app -React Compiler enabled -Starting Metro Bundler -The following packages should be updated for best compatibility with the installed expo version: - expo@54.0.30 - expected version: ~54.0.33 - expo-constants@18.0.12 - expected version: ~18.0.13 - expo-file-system@18.0.12 - expected version: ~19.0.21 - expo-font@14.0.10 - expected version: ~14.0.11 - expo-router@6.0.21 - expected version: ~6.0.23 -Your project may not work correctly until you install the expected versions of the packages. -Waiting on http://localhost:8081 -Logs for your project will appear below. -Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1) -Web node_modules/expo-router/entry.js ▓▓▓▓░░░░░░░░░░░░ 28.0% (16/94) -λ node_modules/expo-router/node/render.js ▓▓░░░░░░░░░░░░░░ 17.4% (121/290) -Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓░░░░░░░░░ 47.1% (173/252) -λ node_modules/expo-router/node/render.js ▓▓▓▓▓▓░░░░░░░░░░ 40.4% (314/494) -Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓░░░░░░░░░ 47.1% (270/399) -λ node_modules/expo-router/node/render.js ▓▓▓▓▓▓▓▓▓░░░░░░░ 61.6% (543/718) -Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓░░░░░░░░░ 49.5% (375/538) -λ node_modules/expo-router/node/render.js ▓▓▓▓▓▓▓▓▓▓▓░░░░░ 70.0% (643/774) -Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓▓▓░░░░░░░ 59.2% (544/714) -λ node_modules/expo-router/node/render.js ▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 80.2% (839/937) -Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 76.5% (740/846) -λ node_modules/expo-router/node/render.js ▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░ 89.8% ( 942/1000) -Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░ 90.0% ( 962/1014) -λ node_modules/expo-router/node/render.js ▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░ 93.4% (1149/1189) -λ Bundled 26965ms node_modules/expo-router/node/render.js (1238 modules) -"shadow*" style props are deprecated. Use "boxShadow". -(node:4029) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. -(Use `node --trace-deprecation ...` to show where the warning was created) -Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░ 99.5% (1284/1287) -Web Bundled 30057ms node_modules/expo-router/entry.js (1287 modules) -Error: Premature close - at onclose (node:internal/streams/end-of-stream:171:30) - at processTicksAndRejections (node:internal/process/task_queues:84:11) -Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1) -λ Bundled 48ms node_modules/expo-router/node/render.js (1 module) -"shadow*" style props are deprecated. Use "boxShadow". -Web Bundled 360ms node_modules/expo-router/entry.js (1 module) -Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1) -λ Bundled 42ms node_modules/expo-router/node/render.js (1 module) -"shadow*" style props are deprecated. Use "boxShadow". -Web Bundled 692ms node_modules/expo-router/entry.js (1 module) -Web node_modules/expo-router/entry.js ▓▓▓▓▓▓░░░░░░░░░░ 37.7% (242/428) -Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓▓▓▓░░░░░░ 63.0% (551/694) -Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓▓▓▓▓▓▓░░░ 85.3% ( 945/1023) -Web Bundled 11818ms node_modules/expo-router/entry.js (1288 modules) - LOG [web] Logs will appear in the browser console -Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1) -λ Bundled 75ms node_modules/expo-router/node/render.js (1 module) -"shadow*" style props are deprecated. Use "boxShadow". -Web Bundled 486ms node_modules/expo-router/entry.js (1 module) -Web Bundled 106ms node_modules/expo-router/entry.js (1 module) - LOG [web] Logs will appear in the browser console -Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1) -λ Bundled 35ms node_modules/expo-router/node/render.js (1 module) -"shadow*" style props are deprecated. Use "boxShadow". -Web Bundled 358ms node_modules/expo-router/entry.js (1 module) -Web Bundled 64ms node_modules/expo-router/entry.js (1 module) - LOG [web] Logs will appear in the browser console -Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1) -λ Bundled 36ms node_modules/expo-router/node/render.js (1 module) -"shadow*" style props are deprecated. Use "boxShadow". -Web Bundled 354ms node_modules/expo-router/entry.js (1 module) -Web Bundled 64ms node_modules/expo-router/entry.js (1 module) - LOG [web] Logs will appear in the browser console -Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1) -λ Bundled 35ms node_modules/expo-router/node/render.js (1 module) -"shadow*" style props are deprecated. Use "boxShadow". -Web Bundled 349ms node_modules/expo-router/entry.js (1 module) -Web Bundled 69ms node_modules/expo-router/entry.js (1 module) - LOG [web] Logs will appear in the browser console -Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1) -λ Bundled 59ms node_modules/expo-router/node/render.js (1 module) -"shadow*" style props are deprecated. Use "boxShadow". -Web Bundled 434ms node_modules/expo-router/entry.js (1 module) -Web Bundled 89ms node_modules/expo-router/entry.js (1 module) - LOG [web] Logs will appear in the browser console -Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1) -λ Bundled 40ms node_modules/expo-router/node/render.js (1 module) -"shadow*" style props are deprecated. Use "boxShadow". -Web Bundled 350ms node_modules/expo-router/entry.js (1 module) -Web Bundled 77ms node_modules/expo-router/entry.js (1 module) - LOG [web] Logs will appear in the browser console -Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1) -λ Bundled 63ms node_modules/expo-router/node/render.js (1 module) -"shadow*" style props are deprecated. Use "boxShadow". -Web Bundled 440ms node_modules/expo-router/entry.js (1 module) -Web Bundled 83ms node_modules/expo-router/entry.js (1 module) - LOG [web] Logs will appear in the browser console -Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1) -λ Bundled 34ms node_modules/expo-router/node/render.js (1 module) -"shadow*" style props are deprecated. Use "boxShadow". -Web Bundled 353ms node_modules/expo-router/entry.js (1 module) -Web Bundled 53ms node_modules/expo-router/entry.js (1 module) - LOG [web] Logs will appear in the browser console