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 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():
@ -732,13 +747,47 @@ class WebServer:
def health(): def health():
return jsonify({"status": "healthy"}), 200 return jsonify({"status": "healthy"}), 200
# Serve React app - This catches all routes not matched by API endpoints # Session route - serve React app for a specific session
@self.app.route('/', defaults={'path': ''}) # @self.app.route('/<session_id>')
@self.app.route('/<path:path>') # def session_route(session_id):
def serve(path): # logging.info(f"{session_id}")
if path != "" and os.path.exists(self.app.static_folder + '/' + path): # # 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) return send_from_directory(self.app.static_folder, path)
else: else:
# 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') 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):
@ -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()

View File

@ -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": {
"supportsTablet": true
},
"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", "resizeMode": "contain",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
} },
] "assetBundlePatterns": [
"**/*"
], ],
"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
}
} }
} }
} }

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", "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",