Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
app:
runs-on: ubuntu-latest
defaults:
run:
working-directory: app
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: app/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm lint

server:
runs-on: ubuntu-latest
defaults:
run:
working-directory: server
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: server/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm test
62 changes: 38 additions & 24 deletions app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,36 @@ import { NavigationContainer } from '@react-navigation/native'
import { Main } from './src/main'
import { useFonts } from 'expo-font'
import { ThemeContext, AppContext } from './src/context'
import * as themes from './src/theme'
import { IMAGE_MODELS, MODELS } from './constants'
import { getTheme } from './src/theme'
import { IMAGE_MODELS, MODELS, fetchModels } from './constants'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { ChatModelModal } from './src/components/index'
import { Model } from './types'
import { Model, Theme } from './types'
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { StatusBar } from 'expo-status-bar'
import {
BottomSheetBackdrop,
BottomSheetModal,
BottomSheetModalProvider,
BottomSheetView,
} from '@gorhom/bottom-sheet'
import { StyleSheet, LogBox } from 'react-native'
import { StyleSheet, LogBox, useColorScheme } from 'react-native'

LogBox.ignoreLogs([
'Key "cancelled" in the image picker result is deprecated and will be removed in SDK 48, use "canceled" instead',
'No native splash screen registered'
])

export default function App() {
const colorScheme = useColorScheme()
const [theme, setTheme] = useState<string>('light')
const [chatType, setChatType] = useState<Model>(MODELS.claudeOpus)
const [imageModel, setImageModel] = useState<string>(IMAGE_MODELS.nanoBanana.label)
const [models, setModels] = useState<Model[]>(Object.values(MODELS))
const [systemPrompt, setSystemPrompt] = useState<string>('')
const [modalVisible, setModalVisible] = useState<boolean>(false)
const [fontsLoaded] = useFonts({
const [fontsLoaded] = useFonts({
'Geist-Regular': require('./assets/fonts/Geist-Regular.otf'),
'Geist-Light': require('./assets/fonts/Geist-Light.otf'),
'Geist-Bold': require('./assets/fonts/Geist-Bold.otf'),
Expand All @@ -43,16 +47,26 @@ export default function App() {

useEffect(() => {
configureStorage()
fetchModels().then(remoteModels => {
setModels(remoteModels)
setChatType(current => remoteModels.find(m => m.label === current.label) || current)
})
}, [])

async function configureStorage() {
try {
const _theme = await AsyncStorage.getItem('rnai-theme')
if (_theme) setTheme(_theme)
const _chatType = await AsyncStorage.getItem('rnai-chatType')
if (_chatType) setChatType(JSON.parse(_chatType))
if (_chatType) {
const parsed = JSON.parse(_chatType)
const model = MODELS[parsed.label]
if (model) setChatType(model)
}
const _imageModel = await AsyncStorage.getItem('rnai-imageModel')
if (_imageModel) setImageModel(_imageModel)
const _systemPrompt = await AsyncStorage.getItem('rnai-systemPrompt')
if (_systemPrompt) setSystemPrompt(_systemPrompt)
} catch (err) {
console.log('error configuring storage', err)
}
Expand All @@ -73,22 +87,28 @@ export default function App() {
}
}

function _setChatType(type) {
function _setChatType(type: Model) {
setChatType(type)
AsyncStorage.setItem('rnai-chatType', JSON.stringify(type))
AsyncStorage.setItem('rnai-chatType', JSON.stringify({ label: type.label }))
}

function _setImageModel(model) {
function _setImageModel(model: string) {
setImageModel(model)
AsyncStorage.setItem('rnai-imageModel', model)
}

function _setTheme(theme) {
function _setTheme(theme: string) {
setTheme(theme)
AsyncStorage.setItem('rnai-theme', theme)
}

const bottomSheetStyles = getBottomsheetStyles(getTheme(theme))
function _setSystemPrompt(prompt: string) {
setSystemPrompt(prompt)
AsyncStorage.setItem('rnai-systemPrompt', prompt)
}

const currentTheme = getTheme(theme, colorScheme)
const bottomSheetStyles = getBottomsheetStyles(currentTheme)

if (!fontsLoaded) return null
return (
Expand All @@ -101,13 +121,17 @@ export default function App() {
imageModel,
setImageModel: _setImageModel,
closeModal,
models,
systemPrompt,
setSystemPrompt: _setSystemPrompt,
}}
>
<ThemeContext.Provider value={{
theme: getTheme(theme),
theme: currentTheme,
themeName: theme,
setTheme: _setTheme
}}>
<StatusBar style={currentTheme.statusBarStyle} />
<ActionSheetProvider>
<NavigationContainer>
<Main />
Expand Down Expand Up @@ -138,7 +162,7 @@ export default function App() {
)
}

const getBottomsheetStyles = theme => StyleSheet.create({
const getBottomsheetStyles = (theme: Theme) => StyleSheet.create({
background: {
paddingHorizontal: 24,
backgroundColor: theme.backgroundColor
Expand All @@ -150,16 +174,6 @@ const getBottomsheetStyles = theme => StyleSheet.create({
borderTopRightRadius: 24,
},
handleIndicator: {
backgroundColor: 'rgba(255, 255, 255, .3)'
backgroundColor: theme.mutedForegroundColor
}
})

function getTheme(theme: any) {
let current
Object.keys(themes).forEach(_theme => {
if (_theme.includes(theme)) {
current = themes[_theme]
}
})
return current
}
100 changes: 94 additions & 6 deletions app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GeminiIcon } from './src/components/GeminiIcon'
import { OpenAIIcon } from './src/components/OpenAIIcon'
import { GLMIcon } from './src/components/GLMIcon'
import { KimiIcon } from './src/components/KimiIcon'
import { Model, Provider } from './types'

const normalizeDomain = (value?: string) => {
if (!value) return ''
Expand All @@ -19,50 +20,137 @@ const rawDomain = env === 'DEVELOPMENT' ? devUrl : prodUrl

export const DOMAIN = normalizeDomain(rawDomain || devUrl || prodUrl || '')

export const MODELS = {
const PROVIDER_ICONS: Record<Provider, Model['icon']> = {
anthropic: AnthropicIcon,
openai: OpenAIIcon,
google: GeminiIcon,
zai: GLMIcon,
moonshot: KimiIcon
}

export function getProviderIcon(provider: Provider): Model['icon'] {
return PROVIDER_ICONS[provider] || GeminiIcon
}

export const MODELS: Record<string, Model> = {
claudeFable5: {
name: 'Claude Fable 5',
label: 'claudeFable5',
provider: 'anthropic',
supportsVision: true,
icon: AnthropicIcon
},
claudeSonnet5: {
name: 'Claude Sonnet 5',
label: 'claudeSonnet5',
provider: 'anthropic',
supportsVision: true,
icon: AnthropicIcon
},
claudeOpus47: {
name: 'Claude Opus 4.7',
label: 'claudeOpus47',
provider: 'anthropic',
supportsVision: true,
icon: AnthropicIcon
},
claudeOpus: {
name: 'Claude Opus',
label: 'claudeOpus',
provider: 'anthropic',
supportsVision: true,
icon: AnthropicIcon
},
claudeSonnet: {
name: 'Claude Sonnet',
label: 'claudeSonnet',
provider: 'anthropic',
supportsVision: true,
icon: AnthropicIcon
},
claudeHaiku: {
name: 'Claude Haiku',
label: 'claudeHaiku',
provider: 'anthropic',
supportsVision: true,
icon: AnthropicIcon
},
claudeSonnet4: {
name: 'Claude Sonnet 4',
label: 'claudeSonnet4',
provider: 'anthropic',
supportsVision: true,
icon: AnthropicIcon
},
gpt52: { name: 'GPT 5.2', label: 'gpt52', icon: OpenAIIcon },
gpt5Mini: { name: 'GPT 5 Mini', label: 'gpt5Mini', icon: OpenAIIcon },
gemini: { name: 'Gemini', label: 'gemini', icon: GeminiIcon },
glm52: { name: 'GLM 5.2', label: 'glm52', icon: GLMIcon },
kimiK27: { name: 'Kimi K2.7', label: 'kimiK27', icon: KimiIcon },
gpt52: {
name: 'GPT 5.2',
label: 'gpt52',
provider: 'openai',
supportsVision: true,
icon: OpenAIIcon
},
gpt5Mini: {
name: 'GPT 5 Mini',
label: 'gpt5Mini',
provider: 'openai',
supportsVision: true,
icon: OpenAIIcon
},
gemini: {
name: 'Gemini',
label: 'gemini',
provider: 'google',
supportsVision: true,
icon: GeminiIcon
},
glm52: {
name: 'GLM 5.2',
label: 'glm52',
provider: 'zai',
supportsVision: false,
icon: GLMIcon
},
kimiK27: {
name: 'Kimi K2.7',
label: 'kimiK27',
provider: 'moonshot',
supportsVision: false,
icon: KimiIcon
}
}

export const IMAGE_MODELS = {
nanoBanana: { name: 'Nano Banana (Gemini Flash Image)', label: 'nanoBanana' },
nanoBananaPro: { name: 'Nano Banana Pro (Gemini 3 Pro)', label: 'nanoBananaPro' },
}

export const PROMPT_SUGGESTIONS = [
'Explain a concept like I am five',
'Help me brainstorm project ideas',
'Write a short story about space travel',
'Summarize the plot of a classic novel'
]

/**
* Fetches the model registry from the server so the server remains the
* single source of truth. Falls back to the local constants when offline.
*/
export async function fetchModels(): Promise<Model[]> {
const fallback = Object.values(MODELS)
if (!DOMAIN) return fallback
try {
const res = await fetch(`${DOMAIN}/models`)
if (!res.ok) return fallback
const data = await res.json()
if (!Array.isArray(data.chatModels) || !data.chatModels.length) return fallback
return data.chatModels.map((m: any) => ({
name: m.name,
label: m.label,
provider: m.provider,
supportsVision: !!m.supportsVision,
icon: getProviderIcon(m.provider)
}))
} catch {
return fallback
}
}
10 changes: 10 additions & 0 deletions app/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require("eslint/config");
const expoConfig = require("eslint-config-expo/flat");

module.exports = defineConfig([
expoConfig,
{
ignores: ["dist/*"],
},
]);
11 changes: 10 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web"
"web": "expo start --web",
"typecheck": "tsc --noEmit",
"lint": "expo lint"
},
"dependencies": {
"@expo/metro-runtime": "~6.1.2",
"@expo/react-native-action-sheet": "^4.0.1",
"@expo/vector-icons": "^15.0.3",
"@gorhom/bottom-sheet": "^5.2.14",
"@react-native-async-storage/async-storage": "3.1.1",
"@react-native-camera-roll/camera-roll": "^7.10.2",
Expand All @@ -23,7 +26,11 @@
"expo-document-picker": "~14.0.8",
"expo-file-system": "~19.0.23",
"expo-font": "~14.0.12",
"expo-haptics": "~15.0.8",
"expo-image-picker": "~17.0.11",
"expo-media-library": "~18.2.1",
"expo-sharing": "~14.0.8",
"expo-speech": "~14.0.8",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
"punycode": "^2.3.1",
Expand All @@ -43,6 +50,8 @@
"devDependencies": {
"@babel/core": "^7.24.0",
"@types/react": "~19.2.17",
"eslint": "^9.39.4",
"eslint-config-expo": "^57.0.0",
"typescript": "~5.9.3"
},
"private": true
Expand Down
Loading
Loading