Added web client
@ -47,7 +47,6 @@ services:
|
||||
volumes:
|
||||
- ./cache:/root/.cache # Cache hub models and neo_compiler_cache
|
||||
- ./ollama:/root/.ollama # Cache the ollama models
|
||||
- ./src:/opt/airc/src:rw # Live mount src
|
||||
cap_add: # used for running ze-monitor within airc container
|
||||
- CAP_DAC_READ_SEARCH # Bypass all filesystem read access checks
|
||||
- CAP_PERFMON # Access to perf_events (vs. overloaded CAP_SYS_ADMIN)
|
||||
@ -69,11 +68,14 @@ services:
|
||||
ports:
|
||||
- 8888:8888 # Jupyter Notebook
|
||||
- 60673:60673 # Gradio
|
||||
- 5000:5000 # Flask React server
|
||||
- 8081:8081 # REACT expo
|
||||
networks:
|
||||
- internal
|
||||
volumes:
|
||||
- ./jupyter:/opt/jupyter:rw
|
||||
- ./cache:/root/.cache
|
||||
- ./src:/opt/airc/src:rw # Live mount src
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
@ -12,6 +12,7 @@ import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
import textwrap
|
||||
import threading
|
||||
|
||||
def try_import(module_name, pip_name=None):
|
||||
try:
|
||||
@ -32,6 +33,8 @@ try_import('dotenv', 'python-dotenv')
|
||||
try_import('geopy', 'geopy')
|
||||
try_import('hyphen', 'PyHyphen')
|
||||
try_import('bs4', 'beautifulsoup4')
|
||||
try_import('flask')
|
||||
try_import('flask_cors')
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from geopy.geocoders import Nominatim
|
||||
@ -44,8 +47,9 @@ 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_cors import CORS
|
||||
|
||||
# Local defined imports
|
||||
from tools import (
|
||||
get_weather_by_location,
|
||||
get_current_datetime,
|
||||
@ -67,17 +71,18 @@ USE_TLS=False
|
||||
GRADIO_HOST="0.0.0.0"
|
||||
GRADIO_PORT=60673
|
||||
GRADIO_ENABLE=False
|
||||
WEB_HOST="0.0.0.0"
|
||||
WEB_PORT=5000
|
||||
WEB_DISABLE=False
|
||||
BOT_ADMIN="james"
|
||||
|
||||
# %%
|
||||
# Globals
|
||||
system_message = f"""
|
||||
You are a helpful information agent connected to the IRC network {IRC_SERVER}. Your name is {NICK}.
|
||||
You have real time access to any website or URL the user asks about.
|
||||
Messages from users are in the form "NICK: MESSAGE". The name before the colon (:) tells you which user asked about something.
|
||||
You have real time access to any website or URL the user asks about, to stock prices, the current date and time, and current weather information for locations in the United States.
|
||||
You are running { { 'model': MODEL_NAME, 'gpu': 'Intel Arc B580', 'cpu': 'Intel Core i9-14900KS', 'ram': '64G' } }.
|
||||
You were launched on {get_current_datetime()}.
|
||||
You have real time access to stock prices, the current date and time, and current weather information for locations in the United States.
|
||||
If you use any real time access, do not mention your knowledge cutoff.
|
||||
Give short, courteous answers, no more than 2-3 sentences.
|
||||
Always be accurate. If you don't know the answer, say so. Do not make up details.
|
||||
@ -108,6 +113,9 @@ def parse_args():
|
||||
parser.add_argument("--gradio-host", type=str, default=GRADIO_HOST, help=f"Host to launch gradio on. default={GRADIO_HOST} only if --gradio-enable is specified.")
|
||||
parser.add_argument("--gradio-port", type=str, default=GRADIO_PORT, help=f"Port to launch gradio on. default={GRADIO_PORT} only if --gradio-enable is specified.")
|
||||
parser.add_argument("--gradio-enable", action="store_true", default=GRADIO_ENABLE, help=f"If set to True, enable Gradio. default={GRADIO_ENABLE}")
|
||||
parser.add_argument("--web-host", type=str, default=WEB_HOST, help=f"Host to launch Flask web server. default={WEB_HOST} only if --web-disable not specified.")
|
||||
parser.add_argument("--web-port", type=str, default=WEB_PORT, help=f"Port to launch Flask web server. default={WEB_PORT} only if --web-disable not specified.")
|
||||
parser.add_argument("--web-disable", action="store_true", default=WEB_DISABLE, help=f"If set to True, disable Flask web server. default={WEB_DISABLE}")
|
||||
parser.add_argument('--level', type=str, choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
||||
default=LOG_LEVEL, help=f'Set the logging level. default={LOG_LEVEL}')
|
||||
return parser.parse_args()
|
||||
@ -310,7 +318,7 @@ async def summarize_site(url, question):
|
||||
except Exception as e:
|
||||
return f"Error processing the website content: {str(e)}"
|
||||
|
||||
async def chat(history, is_irc=False):
|
||||
async def chat(history):
|
||||
global client, model, irc_bot, system_log, tool_log
|
||||
if not client:
|
||||
return history
|
||||
@ -442,14 +450,14 @@ class DynamicIRCBot(pydle.Client):
|
||||
await super().on_part(channel, user)
|
||||
logging.info(f"PART: {channel} => {user}")
|
||||
|
||||
async def on_message(self, target, source, message, is_gradio=False):
|
||||
async def on_message(self, target, source, message, local_user=None):
|
||||
global system_log, tool_log, system_log, command_log
|
||||
|
||||
if not is_gradio:
|
||||
if not local_user:
|
||||
await super().on_message(target, source, message)
|
||||
message = message.strip()
|
||||
logging.info(f"MESSAGE: {source} => {target}: {message}")
|
||||
if source == self.nickname and not is_gradio:
|
||||
if source == self.nickname and not local_user:
|
||||
return
|
||||
last_message = self.history[-1] if len(self.history) > 0 and self.history[-1]["role"] == "user" else None
|
||||
try:
|
||||
@ -493,7 +501,7 @@ class DynamicIRCBot(pydle.Client):
|
||||
"content": f"{source}: {content}"
|
||||
}
|
||||
self.history.append(last_message)
|
||||
self.history = await chat(self.history, is_irc=True)
|
||||
self.history = await chat(self.history)
|
||||
chat_response = self.history[-1]
|
||||
await self.message(target, chat_response['content'])
|
||||
return
|
||||
@ -632,7 +640,7 @@ async def create_ui():
|
||||
if not irc_bot:
|
||||
return gr.skip()
|
||||
await irc_bot.message(irc_bot.channel, f"[console] {message}")
|
||||
await irc_bot.on_message(irc_bot.channel, irc_bot.nickname, f"{irc_bot.nickname}: {message}", is_gradio=True)
|
||||
await irc_bot.on_message(irc_bot.channel, irc_bot.nickname, f"{irc_bot.nickname}: {message}", local_user="gradio")
|
||||
return "", irc_bot.history
|
||||
|
||||
def do_clear():
|
||||
@ -670,6 +678,71 @@ async def create_ui():
|
||||
|
||||
return ui
|
||||
|
||||
# %%
|
||||
class WebServer:
|
||||
"""Web interface"""
|
||||
|
||||
def __init__(self):
|
||||
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"}})
|
||||
|
||||
# Setup routes
|
||||
self.setup_routes()
|
||||
|
||||
def setup_routes(self):
|
||||
"""Setup Flask routes"""
|
||||
|
||||
@self.app.route('/')
|
||||
def serve():
|
||||
return send_from_directory(self.app.static_folder, 'index.html')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
# Basic endpoint for chat completions
|
||||
@self.app.route('/chat', methods=['POST'])
|
||||
async def chat():
|
||||
if not irc_bot:
|
||||
return jsonify({ "error": "Bot not initialized" }), 400
|
||||
try:
|
||||
data = request.get_json()
|
||||
logging.info(f"/chat data={data}")
|
||||
await irc_bot.on_message(irc_bot.channel, irc_bot.nickname, f"{irc_bot.nickname}: {data}", local_user="web")
|
||||
return jsonify(irc_bot.history)
|
||||
except Exception as e:
|
||||
logging.exception(data)
|
||||
return jsonify({
|
||||
"error": "Invalid request"
|
||||
}), 400
|
||||
|
||||
# Context requests
|
||||
@self.app.route('/history', methods=['GET'])
|
||||
def http_history():
|
||||
if not irc_bot:
|
||||
return jsonify({ "error": "Bot not initialized" }), 400
|
||||
return jsonify(irc_bot.history), 200
|
||||
|
||||
@self.app.route('/system', methods=['GET'])
|
||||
def http_system():
|
||||
if not irc_bot:
|
||||
return jsonify({ "error": "Bot not initialized" }), 400
|
||||
return jsonify(system_log), 200
|
||||
|
||||
@self.app.route('/tools', methods=['GET'])
|
||||
def http_tools():
|
||||
if not irc_bot:
|
||||
return jsonify({ "error": "Bot not initialized" }), 400
|
||||
return jsonify(tool_log), 200
|
||||
|
||||
# Health check endpoint
|
||||
@self.app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
return jsonify({"status": "healthy"}), 200
|
||||
|
||||
def run(self, host='0.0.0.0', port=5000, debug=False, **kwargs):
|
||||
"""Run the web server"""
|
||||
# Load documents
|
||||
self.app.run(host=host, port=port, debug=debug, **kwargs)
|
||||
|
||||
# %%
|
||||
|
||||
# Main function to run everything
|
||||
@ -693,6 +766,11 @@ async def main():
|
||||
ui.launch(server_name=args.gradio_host, server_port=args.gradio_port, prevent_thread_lock=True, pwa=True)
|
||||
logging.info(args)
|
||||
|
||||
if not args.web_disable:
|
||||
server = WebServer()
|
||||
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()
|
||||
|
||||
await irc_bot.handle_forever()
|
||||
|
||||
# Run the main function using anyio
|
||||
|
@ -1 +0,0 @@
|
||||
Subproject commit 571cdd1ecc93ec9ad0d63079fe6da94dce2cc5dc
|
38
src/client/.gitignore
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
expo-env.d.ts
|
||||
|
||||
# Native
|
||||
*.orig.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
|
||||
# Metro
|
||||
.metro-health-check*
|
||||
|
||||
# debug
|
||||
npm-debug.*
|
||||
yarn-debug.*
|
||||
yarn-error.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
app-example
|
50
src/client/README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Welcome to your Expo app 👋
|
||||
|
||||
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
|
||||
|
||||
## Get started
|
||||
|
||||
1. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Start the app
|
||||
|
||||
```bash
|
||||
npx expo start
|
||||
```
|
||||
|
||||
In the output, you'll find options to open the app in a
|
||||
|
||||
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
|
||||
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
|
||||
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
|
||||
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
|
||||
|
||||
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
|
||||
|
||||
## Get a fresh project
|
||||
|
||||
When you're ready, run:
|
||||
|
||||
```bash
|
||||
npm run reset-project
|
||||
```
|
||||
|
||||
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
|
||||
|
||||
## Learn more
|
||||
|
||||
To learn more about developing your project with Expo, look at the following resources:
|
||||
|
||||
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
|
||||
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
|
||||
|
||||
## Join the community
|
||||
|
||||
Join our community of developers creating universal apps.
|
||||
|
||||
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
|
||||
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
|
41
src/client/app.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "airc",
|
||||
"slug": "airc",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "myapp",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"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",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
]
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
}
|
||||
}
|
||||
}
|
17
src/client/app/(tabs)/_layout.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
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>
|
||||
);
|
||||
}
|
30
src/client/app/(tabs)/about.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { Text, View, StyleSheet } from 'react-native';
|
||||
|
||||
export default function AboutScreen() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.text}>
|
||||
<div>Welcome to my AI agent. It has live access to websites, weather, and stock information. You can ask it things like:
|
||||
<ul>
|
||||
<li>What's the current weather?</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>
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#25292e',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
color: '#fff',
|
||||
maxWidth: "90%"
|
||||
},
|
||||
});
|
108
src/client/app/(tabs)/index.css
Normal file
@ -0,0 +1,108 @@
|
||||
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;
|
||||
}
|
136
src/client/app/(tabs)/index.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
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}/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}/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;
|
30
src/client/app/+not-found.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
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',
|
||||
},
|
||||
});
|
10
src/client/app/_layout.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { Stack } from 'expo-router';
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
BIN
src/client/assets/fonts/SpaceMono-Regular.ttf
Executable file
BIN
src/client/assets/images/adaptive-icon.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/client/assets/images/favicon.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/client/assets/images/icon.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
src/client/assets/images/partial-react-logo.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
src/client/assets/images/react-logo.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
src/client/assets/images/react-logo@2x.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/client/assets/images/react-logo@3x.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
src/client/assets/images/splash-icon.png
Normal file
After Width: | Height: | Size: 17 KiB |
13841
src/client/package-lock.json
generated
Normal file
57
src/client/package.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "airc",
|
||||
"main": "expo-router/entry",
|
||||
"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"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.0.2",
|
||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||
"@react-navigation/native": "^7.0.14",
|
||||
"expo": "~52.0.37",
|
||||
"expo-blur": "~14.0.3",
|
||||
"expo-constants": "~17.0.7",
|
||||
"expo-font": "~13.0.4",
|
||||
"expo-haptics": "~14.0.1",
|
||||
"expo-linking": "~7.0.5",
|
||||
"expo-router": "~4.0.17",
|
||||
"expo-splash-screen": "~0.29.22",
|
||||
"expo-status-bar": "~2.0.1",
|
||||
"expo-symbols": "~0.2.2",
|
||||
"expo-system-ui": "~4.0.8",
|
||||
"expo-web-browser": "~14.0.2",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-fontawesome": "^1.7.1",
|
||||
"react-native": "0.76.7",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-markdown-display": "^7.0.2",
|
||||
"react-native-reanimated": "~3.16.1",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-web": "~0.19.13",
|
||||
"react-native-webview": "13.12.5",
|
||||
"react-spinners": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/react": "~18.3.12",
|
||||
"@types/react-test-renderer": "^18.3.0",
|
||||
"jest": "^29.2.1",
|
||||
"jest-expo": "~52.0.4",
|
||||
"react-test-renderer": "18.3.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
17
src/client/tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts"
|
||||
]
|
||||
}
|