Pre-expo delete

This commit is contained in:
James Ketr 2025-03-20 13:00:42 -07:00
parent efb30368a1
commit 0a4587437d
10 changed files with 2271 additions and 390 deletions

View File

@ -13,6 +13,7 @@ import time
from datetime import datetime
import textwrap
import threading
import uuid
def try_import(module_name, pip_name=None):
try:
@ -47,7 +48,7 @@ import requests
import yfinance as yf
from hyphen import hyphenator
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 tools import (
@ -682,16 +683,30 @@ async def create_ui():
class WebServer:
"""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='')
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
self.setup_routes()
# Generate a unique session ID
def generate_session_id(self):
return str(uuid.uuid4())
def setup_routes(self):
"""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
@self.app.route('/api/chat', methods=['POST'])
async def chat():
@ -732,14 +747,48 @@ class WebServer:
def health():
return jsonify({"status": "healthy"}), 200
# Serve React app - This catches all routes not matched by API endpoints
@self.app.route('/', defaults={'path': ''})
@self.app.route('/<path:path>')
def serve(path):
if path != "" and os.path.exists(self.app.static_folder + '/' + path):
# 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 static files from the React build folder
@self.app.route('/<session_id>')
def serve_static(session_id):
logging.info(f"Serve request for {session_id}")
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)
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):
"""Run the web server"""
@ -770,7 +819,7 @@ async def main():
logging.info(args)
if not args.web_disable:
server = WebServer()
server = WebServer(logging)
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()

View File

@ -1,41 +1,36 @@
{
"expo": {
"name": "airc",
"slug": "airc",
"name": "Ketr-Chat",
"slug": "ketr-chat",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "myapp",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"supportsTablet": true
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"android": {
"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"
}
]
"assetBundlePatterns": [
"**/*"
],
"web": {
"favicon": "./assets/favicon.png",
"bundler": "metro"
},
"scheme": "myapp",
"packagerOpts": {
"hostType": "lan",
"dev": true,
"minify": false
},
"experiments": {
"typedRoutes": true
"tsconfigPaths": true
},
"extra": {
"router": {
"origin": false
}
}
}
}

View File

@ -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>
);
}

View File

@ -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%"
},
});

View File

@ -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;
}

View File

@ -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;

View File

@ -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',
},
});

View File

@ -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>
</>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -4,18 +4,17 @@
"version": "1.0.0",
"scripts": {
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"test": "jest --watchAll",
"lint": "expo lint"
"direct": "react-native start --reset-cache",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web"
},
"jest": {
"preset": "jest-expo"
},
"dependencies": {
"@expo/vector-icons": "^14.0.2",
"@react-native/metro-config": "^0.78.1",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"expo": "~52.0.37",
@ -46,6 +45,7 @@
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@react-native-community/cli": "^18.0.0",
"@types/jest": "^29.5.12",
"@types/react": "~18.3.12",
"@types/react-test-renderer": "^18.3.0",