Pre-expo delete
This commit is contained in:
parent
efb30368a1
commit
0a4587437d
@ -13,6 +13,7 @@ import time
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import textwrap
|
import textwrap
|
||||||
import threading
|
import threading
|
||||||
|
import uuid
|
||||||
|
|
||||||
def try_import(module_name, pip_name=None):
|
def try_import(module_name, pip_name=None):
|
||||||
try:
|
try:
|
||||||
@ -47,7 +48,7 @@ import requests
|
|||||||
import yfinance as yf
|
import yfinance as yf
|
||||||
from hyphen import hyphenator
|
from hyphen import hyphenator
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from flask import Flask, request, jsonify, render_template, send_from_directory
|
from flask import Flask, request, jsonify, render_template, send_from_directory, redirect
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
|
|
||||||
from tools import (
|
from tools import (
|
||||||
@ -682,16 +683,30 @@ async def create_ui():
|
|||||||
class WebServer:
|
class WebServer:
|
||||||
"""Web interface"""
|
"""Web interface"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, logging):
|
||||||
|
self.logging = logging
|
||||||
self.app = Flask(__name__, static_folder='/opt/airc/src/client/dist', static_url_path='')
|
self.app = Flask(__name__, static_folder='/opt/airc/src/client/dist', static_url_path='')
|
||||||
CORS(self.app, resources={r"/*": {"origins": "http://battle-linux.ketrenos.com:8081"}})
|
CORS(self.app, resources={r"/*": {"origins": "http://battle-linux.ketrenos.com:5000"}})
|
||||||
|
|
||||||
# Setup routes
|
# Setup routes
|
||||||
self.setup_routes()
|
self.setup_routes()
|
||||||
|
|
||||||
|
# Generate a unique session ID
|
||||||
|
def generate_session_id(self):
|
||||||
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
def setup_routes(self):
|
def setup_routes(self):
|
||||||
"""Setup Flask routes"""
|
"""Setup Flask routes"""
|
||||||
|
|
||||||
|
# Serve React app - This catches all routes not matched by API endpoints
|
||||||
|
@self.app.route('/')
|
||||||
|
def root():
|
||||||
|
# Generate a new unique session ID
|
||||||
|
session_id = self.generate_session_id()
|
||||||
|
# Redirect to the unique session path
|
||||||
|
self.logging.info(f"Redirecting non-session to {session_id}")
|
||||||
|
return redirect(f'/{session_id}')
|
||||||
|
|
||||||
# Basic endpoint for chat completions
|
# Basic endpoint for chat completions
|
||||||
@self.app.route('/api/chat', methods=['POST'])
|
@self.app.route('/api/chat', methods=['POST'])
|
||||||
async def chat():
|
async def chat():
|
||||||
@ -731,15 +746,49 @@ class WebServer:
|
|||||||
@self.app.route('/api/health', methods=['GET'])
|
@self.app.route('/api/health', methods=['GET'])
|
||||||
def health():
|
def health():
|
||||||
return jsonify({"status": "healthy"}), 200
|
return jsonify({"status": "healthy"}), 200
|
||||||
|
|
||||||
|
# Session route - serve React app for a specific session
|
||||||
|
# @self.app.route('/<session_id>')
|
||||||
|
# def session_route(session_id):
|
||||||
|
# logging.info(f"{session_id}")
|
||||||
|
# # Validate if session_id is a valid UUID format (optional)
|
||||||
|
# try:
|
||||||
|
# uuid.UUID(session_id)
|
||||||
|
# # Here you could look up session data in a database if needed
|
||||||
|
# return send_from_directory(
|
||||||
|
# self.app.static_folder,
|
||||||
|
# 'index.html',
|
||||||
|
# mimetype='text/html'
|
||||||
|
# )
|
||||||
|
# except ValueError:
|
||||||
|
# # If not a valid UUID, it might be another path
|
||||||
|
# if os.path.exists(self.app.static_folder + '/' + session_id):
|
||||||
|
# return send_from_directory(self.app.static_folder, session_id)
|
||||||
|
# else:
|
||||||
|
# return send_from_directory(self.app.static_folder, 'index.html')
|
||||||
|
|
||||||
# Serve React app - This catches all routes not matched by API endpoints
|
# Serve static files from the React build folder
|
||||||
@self.app.route('/', defaults={'path': ''})
|
@self.app.route('/<session_id>')
|
||||||
@self.app.route('/<path:path>')
|
def serve_static(session_id):
|
||||||
def serve(path):
|
logging.info(f"Serve request for {session_id}")
|
||||||
if path != "" and os.path.exists(self.app.static_folder + '/' + path):
|
path = session_id
|
||||||
|
if os.path.exists(os.path.join(self.app.static_folder, path)):
|
||||||
|
# If the file exists, serve it with the correct MIME type
|
||||||
return send_from_directory(self.app.static_folder, path)
|
return send_from_directory(self.app.static_folder, path)
|
||||||
else:
|
else:
|
||||||
return send_from_directory(self.app.static_folder, 'index.html')
|
# For nested paths like 'static/js/main.js'
|
||||||
|
parts = path.split('/')
|
||||||
|
session_id = parts[0]
|
||||||
|
rest_path = '/'.join(parts[1:])
|
||||||
|
if not rest_path:
|
||||||
|
rest_path = 'index.html'
|
||||||
|
if os.path.exists(os.path.join(self.app.static_folder, rest_path)):
|
||||||
|
self.logging.info(f"Serving '{rest_path}' for {session_id}")
|
||||||
|
return send_from_directory(self.app.static_folder, rest_path)
|
||||||
|
|
||||||
|
# Default to serving index.html
|
||||||
|
logging.info("Fall through to index.html")
|
||||||
|
return send_from_directory(self.app.static_folder, 'index.html')
|
||||||
|
|
||||||
def run(self, host='0.0.0.0', port=5000, debug=False, **kwargs):
|
def run(self, host='0.0.0.0', port=5000, debug=False, **kwargs):
|
||||||
"""Run the web server"""
|
"""Run the web server"""
|
||||||
@ -770,7 +819,7 @@ async def main():
|
|||||||
logging.info(args)
|
logging.info(args)
|
||||||
|
|
||||||
if not args.web_disable:
|
if not args.web_disable:
|
||||||
server = WebServer()
|
server = WebServer(logging)
|
||||||
logging.info(f"Starting web server at http://{args.web_host}:{args.web_port}")
|
logging.info(f"Starting web server at http://{args.web_host}:{args.web_port}")
|
||||||
threading.Thread(target=lambda: server.run(host=args.web_host, port=args.web_port, debug=True, use_reloader=False)).start()
|
threading.Thread(target=lambda: server.run(host=args.web_host, port=args.web_port, debug=True, use_reloader=False)).start()
|
||||||
|
|
||||||
|
@ -1,41 +1,36 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "airc",
|
"name": "Ketr-Chat",
|
||||||
"slug": "airc",
|
"slug": "ketr-chat",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"scheme": "myapp",
|
"userInterfaceStyle": "light",
|
||||||
"userInterfaceStyle": "automatic",
|
"splash": {
|
||||||
"newArchEnabled": true,
|
"image": "./assets/splash.png",
|
||||||
"ios": {
|
"resizeMode": "contain",
|
||||||
"supportsTablet": true
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"android": {
|
"assetBundlePatterns": [
|
||||||
"adaptiveIcon": {
|
"**/*"
|
||||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
|
||||||
"backgroundColor": "#ffffff"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web": {
|
|
||||||
"bundler": "metro",
|
|
||||||
"output": "static",
|
|
||||||
"favicon": "./assets/images/favicon.png"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"expo-router",
|
|
||||||
[
|
|
||||||
"expo-splash-screen",
|
|
||||||
{
|
|
||||||
"image": "./assets/images/splash-icon.png",
|
|
||||||
"imageWidth": 200,
|
|
||||||
"resizeMode": "contain",
|
|
||||||
"backgroundColor": "#ffffff"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
],
|
||||||
|
"web": {
|
||||||
|
"favicon": "./assets/favicon.png",
|
||||||
|
"bundler": "metro"
|
||||||
|
},
|
||||||
|
"scheme": "myapp",
|
||||||
|
"packagerOpts": {
|
||||||
|
"hostType": "lan",
|
||||||
|
"dev": true,
|
||||||
|
"minify": false
|
||||||
|
},
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true
|
"tsconfigPaths": true
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"router": {
|
||||||
|
"origin": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,17 +0,0 @@
|
|||||||
import { Tabs } from 'expo-router';
|
|
||||||
import FontAwesome from 'react-fontawesome';
|
|
||||||
|
|
||||||
export default function TabLayout() {
|
|
||||||
return (
|
|
||||||
<Tabs>
|
|
||||||
<Tabs.Screen name="index" options={{
|
|
||||||
title: 'Chat',
|
|
||||||
tabBarIcon: ({ color }) => <FontAwesome size={28} name="chat" color={color} />
|
|
||||||
}} />
|
|
||||||
<Tabs.Screen name="about" options={{
|
|
||||||
title: 'About',
|
|
||||||
tabBarIcon: ({ color }) => <FontAwesome size={28} name="about" color={color} />
|
|
||||||
}} />
|
|
||||||
</Tabs>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
import { Text, View, StyleSheet } from 'react-native';
|
|
||||||
|
|
||||||
export default function AboutScreen() {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text style={styles.text}>
|
|
||||||
<div>Welcome to Ketr-AI. This AI agent has live access to websites, weather, and stock information. You can ask it things like:
|
|
||||||
<ul>
|
|
||||||
<li>What's the current weather in Kansas?</li>
|
|
||||||
<li>Can you provide the current headlines from http://cnn.com?</li>
|
|
||||||
<li>What is the current value of the 5 most traded companies?</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>Internally, the system is using the LLAMA3.2 large language model, currently running locally in ollama. Various tools have been enabled for the LLM to use.</div>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: '#25292e',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
color: '#fff',
|
|
||||||
maxWidth: "90%"
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,108 +0,0 @@
|
|||||||
body {
|
|
||||||
font-family: 'Poppins', sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
max-height: 100vh;
|
|
||||||
overflow: auto;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.query-box {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.query-box input {
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.query-box button {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background-color: #4CAF50;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation {
|
|
||||||
background-color: #F5F5F5;
|
|
||||||
border: 1px solid #E0E0E0;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-message {
|
|
||||||
background-color: #DCF8C6;
|
|
||||||
border: 1px solid #B2E0A7;
|
|
||||||
color: #333333;
|
|
||||||
padding: 0.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
margin-left: 1rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
min-width: 70%;
|
|
||||||
max-width: 70%;
|
|
||||||
justify-self: right;
|
|
||||||
display: flex;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.assistant-message {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
border: 1px solid #E0E0E0;
|
|
||||||
color: #333333;
|
|
||||||
padding: 0.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
margin-right: 1rem;
|
|
||||||
min-width: 70%;
|
|
||||||
max-width: 70%;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
justify-self: left;
|
|
||||||
display: flex;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
padding: 8px 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background-color: #f1f1f1;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab.active {
|
|
||||||
background-color: #4CAF50;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content {
|
|
||||||
display: none;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
padding: 10px;
|
|
||||||
height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
@ -1,136 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef, CSSProperties } from 'react';
|
|
||||||
import PropagateLoader from "react-spinners/PropagateLoader";
|
|
||||||
import Markdown from 'react-native-markdown-display';
|
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
const url: string = "https://ai.ketrenos.com"
|
|
||||||
const override: CSSProperties = {
|
|
||||||
display: "block",
|
|
||||||
margin: "0 auto",
|
|
||||||
borderColor: "red",
|
|
||||||
};
|
|
||||||
const App = () => {
|
|
||||||
const [query, setQuery] = useState('');
|
|
||||||
const [conversation, setConversation] = useState([]);
|
|
||||||
const conversationRef = useRef(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
// Scroll to bottom of conversation when conversation updates
|
|
||||||
useEffect(() => {
|
|
||||||
if (conversationRef.current) {
|
|
||||||
conversationRef.current.scrollTop = conversationRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
}, [conversation]);
|
|
||||||
|
|
||||||
|
|
||||||
// Polling interval in milliseconds (e.g., 5 seconds)
|
|
||||||
const pollingInterval = 3000;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchHistory = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${url}/api/history`);
|
|
||||||
const data = await response.json();
|
|
||||||
if (conversation.length != data.length)
|
|
||||||
setConversation(data || []);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching documents:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial fetch
|
|
||||||
fetchHistory();
|
|
||||||
|
|
||||||
// Set interval for polling
|
|
||||||
const intervalId = setInterval(fetchHistory, pollingInterval);
|
|
||||||
|
|
||||||
// Cleanup on component unmount
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
}, [conversation, setConversation, pollingInterval]);
|
|
||||||
|
|
||||||
const handleKeyPress = (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
sendQuery();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendQuery = async () => {
|
|
||||||
if (!query.trim()) return;
|
|
||||||
|
|
||||||
// Add user message to conversation
|
|
||||||
const newConversation = [
|
|
||||||
...conversation,
|
|
||||||
{ role: 'user', content: query }
|
|
||||||
];
|
|
||||||
setConversation(newConversation);
|
|
||||||
|
|
||||||
// Clear input
|
|
||||||
setQuery('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
// Send query to server
|
|
||||||
const response = await fetch(`${url}/api/chat`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(query.trim()),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
// Add assistant message to conversation
|
|
||||||
setConversation(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error:', error);
|
|
||||||
setConversation([
|
|
||||||
...newConversation,
|
|
||||||
{ role: 'assistant', content: 'Error processing your query. Please try again.' }
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container">
|
|
||||||
<div className="conversation" ref={conversationRef}>
|
|
||||||
{conversation.map((msg, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={msg.role === 'user' ? 'user-message' : 'assistant-message'}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
msg.content
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => line.replace(/^[^\s:]+:\s*/, ''))
|
|
||||||
.join("\n")
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
))}
|
|
||||||
<div style={{justifyContent: "center", display: "flex", paddingBottom: "0.5rem"}}>
|
|
||||||
<PropagateLoader
|
|
||||||
size="10px"
|
|
||||||
loading={loading}
|
|
||||||
aria-label="Loading Spinner"
|
|
||||||
data-testid="loader"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="query-box">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={query}
|
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
placeholder="Enter your query..."
|
|
||||||
id="query-input"
|
|
||||||
/>
|
|
||||||
<button onClick={sendQuery}>Send</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
@ -1,30 +0,0 @@
|
|||||||
import { View, StyleSheet } from 'react-native';
|
|
||||||
import { Link, Stack } from 'expo-router';
|
|
||||||
|
|
||||||
export default function NotFoundScreen() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Stack.Screen options={{ title: 'Oops! Not Found' }} />
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Link href="/" style={styles.button}>
|
|
||||||
Go back to Home screen!
|
|
||||||
</Link>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: '#25292e',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
|
|
||||||
button: {
|
|
||||||
fontSize: 20,
|
|
||||||
textDecorationLine: 'underline',
|
|
||||||
color: '#fff',
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,18 +0,0 @@
|
|||||||
import { Stack } from 'expo-router';
|
|
||||||
import { Helmet } from 'react-helmet';
|
|
||||||
|
|
||||||
export default function RootLayout() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Helmet>
|
|
||||||
<title>Ketr-AI for Everyone</title>
|
|
||||||
<meta name="description" content="Ketr-AI for Everyone" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Helmet>
|
|
||||||
<Stack>
|
|
||||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen name="+not-found" />
|
|
||||||
</Stack>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
2179
src/client/package-lock.json
generated
2179
src/client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,18 +4,17 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"reset-project": "node ./scripts/reset-project.js",
|
"direct": "react-native start --reset-cache",
|
||||||
"android": "expo start --android",
|
"android": "expo run:android",
|
||||||
"ios": "expo start --ios",
|
"ios": "expo run:ios",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web"
|
||||||
"test": "jest --watchAll",
|
|
||||||
"lint": "expo lint"
|
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "jest-expo"
|
"preset": "jest-expo"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
|
"@react-native/metro-config": "^0.78.1",
|
||||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||||
"@react-navigation/native": "^7.0.14",
|
"@react-navigation/native": "^7.0.14",
|
||||||
"expo": "~52.0.37",
|
"expo": "~52.0.37",
|
||||||
@ -46,6 +45,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
"@react-native-community/cli": "^18.0.0",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/react": "~18.3.12",
|
"@types/react": "~18.3.12",
|
||||||
"@types/react-test-renderer": "^18.3.0",
|
"@types/react-test-renderer": "^18.3.0",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user