Spaces:
Sleeping
Sleeping
Commit
·
5e0532d
1
Parent(s):
b626375
Initial ORA deployment
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .claude/settings.local.json +9 -0
- .gitattributes +2 -34
- .gitignore +37 -0
- Dockerfile +31 -0
- ORA_Training_Colab.ipynb +93 -0
- README.md +28 -10
- app/agents/base.py +18 -0
- app/agents/gatekeeper.py +26 -0
- app/agents/healer.py +14 -0
- app/agents/theologian.py +46 -0
- app/api/routes.py +62 -0
- app/core/config.py +37 -0
- app/core/swarm_client.py +149 -0
- app/ora_server.py +107 -0
- app/services/audit.py +51 -0
- app/services/bible.py +97 -0
- app/services/blessing.py +15 -0
- app/services/citations.py +24 -0
- app/services/context.py +21 -0
- app/services/discernment.py +27 -0
- app/services/doctrine.py +21 -0
- app/services/emotion.py +66 -0
- app/services/ethics.py +23 -0
- app/services/feedback.py +11 -0
- app/services/growth.py +20 -0
- app/services/guardrails.py +61 -0
- app/services/intent.py +60 -0
- app/services/journal.py +86 -0
- app/services/llm.py +128 -0
- app/services/memory.py +169 -0
- app/services/orchestrator.py +58 -0
- app/services/practices.py +19 -0
- app/services/prayer.py +22 -0
- app/services/profile.py +107 -0
- app/services/repl.py +89 -0
- app/services/rlm.py +48 -0
- app/services/simplify.py +19 -0
- app/services/stillness.py +17 -0
- app/services/trace.py +31 -0
- app/services/translation.py +24 -0
- config.yml +1 -0
- frontend/.claude/settings.local.json +17 -0
- frontend/.gitignore +33 -0
- frontend/README.md +98 -0
- frontend/app/about/page.tsx +182 -0
- frontend/app/candle/page.tsx +635 -0
- frontend/app/contact/page.tsx +121 -0
- frontend/app/dashboard/bible/page.tsx +143 -0
- frontend/app/dashboard/explore/page.tsx +233 -0
- frontend/app/dashboard/insights/page.tsx +222 -0
.claude/settings.local.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"permissions": {
|
| 3 |
+
"allow": [
|
| 4 |
+
"Bash(npm install:*)",
|
| 5 |
+
"Bash(npm run build:*)",
|
| 6 |
+
"Bash(npm run dev:*)"
|
| 7 |
+
]
|
| 8 |
+
}
|
| 9 |
+
}
|
.gitattributes
CHANGED
|
@@ -1,35 +1,3 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
important/finetuning/models/ora_adapter/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
data/**/*.lance filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment files
|
| 2 |
+
.env
|
| 3 |
+
*.env
|
| 4 |
+
|
| 5 |
+
# Python
|
| 6 |
+
__pycache__/
|
| 7 |
+
*.py[cod]
|
| 8 |
+
*$py.class
|
| 9 |
+
venv/
|
| 10 |
+
.venv/
|
| 11 |
+
.pytest_cache/
|
| 12 |
+
|
| 13 |
+
# Node/Frontend
|
| 14 |
+
node_modules/
|
| 15 |
+
dist/
|
| 16 |
+
.next/
|
| 17 |
+
out/
|
| 18 |
+
build/
|
| 19 |
+
|
| 20 |
+
# ORA specific - Large data & Databases
|
| 21 |
+
important/curated_data/
|
| 22 |
+
important/finetuning/models/*
|
| 23 |
+
!important/finetuning/models/ora_adapter/
|
| 24 |
+
important/finetuning/logs/
|
| 25 |
+
*.jsonl
|
| 26 |
+
memory.db
|
| 27 |
+
*.zip
|
| 28 |
+
*.log
|
| 29 |
+
|
| 30 |
+
# IDEs
|
| 31 |
+
.vscode/
|
| 32 |
+
.idea/
|
| 33 |
+
.DS_Store
|
| 34 |
+
|
| 35 |
+
# Artifacts
|
| 36 |
+
C:\Users\max\.gemini\antigravity\brain\
|
| 37 |
+
important/documentation/
|
Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# 1. Install Node.js
|
| 6 |
+
RUN apt-get update && apt-get install -y curl gnupg
|
| 7 |
+
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
|
| 8 |
+
RUN apt-get install -y nodejs
|
| 9 |
+
|
| 10 |
+
# 2. Copy Files
|
| 11 |
+
COPY . /app
|
| 12 |
+
|
| 13 |
+
# 3. Build Frontend
|
| 14 |
+
WORKDIR /app/frontend
|
| 15 |
+
RUN npm install
|
| 16 |
+
RUN npm run build
|
| 17 |
+
WORKDIR /app
|
| 18 |
+
|
| 19 |
+
# 4. Install Python Backend Dependencies
|
| 20 |
+
RUN pip install torch transformers peft fastapi uvicorn unsloth[colab-new]
|
| 21 |
+
|
| 22 |
+
# HF Spaces User ID Setup (Optional but recommended)
|
| 23 |
+
RUN useradd -m -u 1000 user
|
| 24 |
+
USER user
|
| 25 |
+
ENV HOME=/home/user \
|
| 26 |
+
PATH=/home/user/.local/bin:$PATH
|
| 27 |
+
|
| 28 |
+
WORKDIR /app
|
| 29 |
+
|
| 30 |
+
# 5. Start Backend (which serves Frontend)
|
| 31 |
+
CMD ["python", "app/ora_server.py"]
|
ORA_Training_Colab.ipynb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {
|
| 6 |
+
"id": "introduction"
|
| 7 |
+
},
|
| 8 |
+
"source": [
|
| 9 |
+
"# ORA Fine-tuning on Google Colab\n",
|
| 10 |
+
"This notebook allows you to fine-tune the ORA model using Unsloth and QLoRA for high-performance training on NVIDIA GPUs.\n",
|
| 11 |
+
"\n",
|
| 12 |
+
"## Setup\n",
|
| 13 |
+
"1. Go to **Runtime** > **Change runtime type** and ensure **GPU** (T4 or A100) is selected.\n",
|
| 14 |
+
"2. Run the following cells to install dependencies and load your curated data."
|
| 15 |
+
]
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"cell_type": "code",
|
| 19 |
+
"execution_count": null,
|
| 20 |
+
"metadata": {
|
| 21 |
+
"id": "install-deps"
|
| 22 |
+
},
|
| 23 |
+
"outputs": [],
|
| 24 |
+
"source": [
|
| 25 |
+
"!pip install unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git\n",
|
| 26 |
+
"!pip install --no-deps \"xformers<0.0.29\" \"trl<0.9.0\" peft accelerate bitsandbytes"
|
| 27 |
+
]
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"cell_type": "markdown",
|
| 31 |
+
"metadata": {
|
| 32 |
+
"id": "upload-data"
|
| 33 |
+
},
|
| 34 |
+
"source": [
|
| 35 |
+
"## Upload Data\n",
|
| 36 |
+
"Upload the `ora_colab_training.zip` file generated by `scripts/prepare_colab.py`."
|
| 37 |
+
]
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"cell_type": "code",
|
| 41 |
+
"execution_count": null,
|
| 42 |
+
"metadata": {
|
| 43 |
+
"id": "unzip-data"
|
| 44 |
+
},
|
| 45 |
+
"outputs": [],
|
| 46 |
+
"source": [
|
| 47 |
+
"!unzip ora_colab_training.zip"
|
| 48 |
+
]
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"cell_type": "markdown",
|
| 52 |
+
"metadata": {
|
| 53 |
+
"id": "training"
|
| 54 |
+
},
|
| 55 |
+
"source": [
|
| 56 |
+
"## Start Training\n",
|
| 57 |
+
"Run the training script."
|
| 58 |
+
]
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"cell_type": "code",
|
| 62 |
+
"execution_count": null,
|
| 63 |
+
"metadata": {
|
| 64 |
+
"id": "run-training"
|
| 65 |
+
},
|
| 66 |
+
"outputs": [],
|
| 67 |
+
"source": [
|
| 68 |
+
"!python scripts/train_ora.py"
|
| 69 |
+
]
|
| 70 |
+
}
|
| 71 |
+
],
|
| 72 |
+
"metadata": {
|
| 73 |
+
"kernelspec": {
|
| 74 |
+
"display_name": "Python 3",
|
| 75 |
+
"language": "python",
|
| 76 |
+
"name": "python3"
|
| 77 |
+
},
|
| 78 |
+
"language_info": {
|
| 79 |
+
"codemirror_mode": {
|
| 80 |
+
"name": "ipython",
|
| 81 |
+
"version": 3
|
| 82 |
+
},
|
| 83 |
+
"file_extension": ".py",
|
| 84 |
+
"mimetype": "text/x-python",
|
| 85 |
+
"name": "python",
|
| 86 |
+
"nbconvert_exporter": "python",
|
| 87 |
+
"pygments_lexer": "ipython3",
|
| 88 |
+
"version": "3.10.12"
|
| 89 |
+
}
|
| 90 |
+
},
|
| 91 |
+
"nbformat": 4,
|
| 92 |
+
"nbformat_minor": 4
|
| 93 |
+
}
|
README.md
CHANGED
|
@@ -1,10 +1,28 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ORA: AI Spiritual Assistant (Python Edition)
|
| 2 |
+
|
| 3 |
+
ORA is a high-performance AI microservice built with **FastAPI** to serve as the spiritual intelligence layer for the SoapBox ecosystem.
|
| 4 |
+
|
| 5 |
+
## Architecture
|
| 6 |
+
- **Framework**: FastAPI (Python 3.10+)
|
| 7 |
+
- **Inference**: Asynchronous OpenAI-compatible client (works with Ollama, vLLM, DeepSeek).
|
| 8 |
+
- **Documentation**: Automatic Swagger UI at `/docs`.
|
| 9 |
+
|
| 10 |
+
## Setup
|
| 11 |
+
1. **Prerequisites**: Python 3.10+, [Ollama](https://ollama.com) (recommended).
|
| 12 |
+
2. **Install**:
|
| 13 |
+
```bash
|
| 14 |
+
python -m venv venv
|
| 15 |
+
source venv/bin/activate # Windows: venv\Scripts\activate
|
| 16 |
+
pip install -r requirements.txt
|
| 17 |
+
```
|
| 18 |
+
3. **Run**:
|
| 19 |
+
```bash
|
| 20 |
+
python main.py
|
| 21 |
+
```
|
| 22 |
+
The server starts at `http://localhost:6000`.
|
| 23 |
+
|
| 24 |
+
## API Endpoints
|
| 25 |
+
- `POST /api/chat`: Send a message.
|
| 26 |
+
- Body: `{"message": "Hello ORA"}`
|
| 27 |
+
- `GET /health`: Health check.
|
| 28 |
+
- `GET /docs`: Interactive API playground.
|
app/agents/base.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Callable, Optional, Union, Dict
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
|
| 4 |
+
class Agent(BaseModel):
|
| 5 |
+
name: str = "Agent"
|
| 6 |
+
model: str = "llama3"
|
| 7 |
+
instructions: Union[str, Callable] = "You are a helpful assistant."
|
| 8 |
+
functions: List[Callable] = []
|
| 9 |
+
context_variables: Dict = {}
|
| 10 |
+
|
| 11 |
+
class Config:
|
| 12 |
+
arbitrary_types_allowed = True
|
| 13 |
+
|
| 14 |
+
class Response(BaseModel):
|
| 15 |
+
agent: Optional[Agent]
|
| 16 |
+
messages: List[Dict]
|
| 17 |
+
context_variables: Dict = {}
|
| 18 |
+
trace: List[Dict] = []
|
app/agents/gatekeeper.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.agents.base import Agent
|
| 2 |
+
|
| 3 |
+
def transfer_to_theologian():
|
| 4 |
+
"""Call this when the user wants to study the Bible, explain doctrine, or analyze scripture."""
|
| 5 |
+
from app.agents.theologian import theologian_agent
|
| 6 |
+
return theologian_agent
|
| 7 |
+
|
| 8 |
+
def transfer_to_healer():
|
| 9 |
+
"""Call this when the user is emotional, needs prayer, or is seeking personal comfort."""
|
| 10 |
+
from app.agents.healer import healer_agent
|
| 11 |
+
return healer_agent
|
| 12 |
+
|
| 13 |
+
gatekeeper_agent = Agent(
|
| 14 |
+
name="Gatekeeper",
|
| 15 |
+
instructions=lambda context: f"""
|
| 16 |
+
You are the ORA Gatekeeper. Your job is to listen to the user and hand them off to the right specialist.
|
| 17 |
+
|
| 18 |
+
IMPORTANT CONTEXT (Episodic Memory):
|
| 19 |
+
{context.get('episodic_memory', 'No relevant past insights found.')}
|
| 20 |
+
|
| 21 |
+
- If they want to STUDY or explore complex Bible topics, call transfer_to_theologian.
|
| 22 |
+
- If they are HURTING, need prayer, or want to talk about their feelings, call transfer_to_healer.
|
| 23 |
+
- If it is general conversation, use the past insights above to show you remember their journey, then ask how you can help them grow today.
|
| 24 |
+
""",
|
| 25 |
+
functions=[transfer_to_theologian, transfer_to_healer]
|
| 26 |
+
)
|
app/agents/healer.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.agents.base import Agent
|
| 2 |
+
# In the future, we can import prayer_service or similar
|
| 3 |
+
|
| 4 |
+
healer_agent = Agent(
|
| 5 |
+
name="Healer",
|
| 6 |
+
model="christian-bible-expert", # Target the empathetic expert model
|
| 7 |
+
instructions="""
|
| 8 |
+
You are the ORA Healer. You focus on emotional support, prayer, and spiritual comfort.
|
| 9 |
+
- Listen deeply to the user's pain or joy.
|
| 10 |
+
- Offer heartfelt prayers based on their situation.
|
| 11 |
+
- Use scripture to provide comfort, but focus on the relationship and the heart.
|
| 12 |
+
- If the user wants a deep theological debate or technical study, hand back to the Gatekeeper.
|
| 13 |
+
"""
|
| 14 |
+
)
|
app/agents/theologian.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.agents.base import Agent
|
| 2 |
+
from app.services.bible import bible_service
|
| 3 |
+
from app.services.journal import journal_service
|
| 4 |
+
from app.services.rlm import rlm_service
|
| 5 |
+
|
| 6 |
+
async def search_bible(query: str):
|
| 7 |
+
"""Search the Bible for relevant verses and context."""
|
| 8 |
+
results = await bible_service.search_bible(query)
|
| 9 |
+
if not results:
|
| 10 |
+
return "No relevant verses found."
|
| 11 |
+
formatted = "\n".join([f"- {r['reference']}: {r['text']}" for r in results])
|
| 12 |
+
return f"I found these relevant passages:\n{formatted}"
|
| 13 |
+
|
| 14 |
+
async def search_journal(context_variables: dict, query: str):
|
| 15 |
+
"""Search the user's past journal entries and reflections for related insights."""
|
| 16 |
+
user_id = context_variables.get("user_id", "unknown")
|
| 17 |
+
related = await journal_service.get_related_entries(user_id, query, limit=2)
|
| 18 |
+
if not related:
|
| 19 |
+
return "No related journal entries found."
|
| 20 |
+
|
| 21 |
+
res = "Found related past reflections:\n"
|
| 22 |
+
for r in related:
|
| 23 |
+
res += f"- {r['timestamp']}: {r['text']}\n"
|
| 24 |
+
return res
|
| 25 |
+
|
| 26 |
+
async def execute_logic(problem: str):
|
| 27 |
+
"""
|
| 28 |
+
Execute a logical reasoning loop to solve complex calculations, timelines, or consistency checks.
|
| 29 |
+
Use this when you need an objective 'workspace' to verify facts or dates.
|
| 30 |
+
"""
|
| 31 |
+
return await rlm_service.reason(problem)
|
| 32 |
+
|
| 33 |
+
theologian_agent = Agent(
|
| 34 |
+
name="Theologian",
|
| 35 |
+
model="gabriel-8x7b",
|
| 36 |
+
instructions="""
|
| 37 |
+
You are the ORA Theologian. You specialize in Bible study, doctrine, and scripture analysis.
|
| 38 |
+
Your goal is to provide deep, accurate, and faithful explanations of the Bible.
|
| 39 |
+
- Use search_bible to find matching verses.
|
| 40 |
+
- Use search_journal to see if the user has reflected on this topic before.
|
| 41 |
+
- Use execute_logic if you need to verify timelines, dates, or complex logical steps.
|
| 42 |
+
- Always provide chapter and verse citations.
|
| 43 |
+
- Avoid making authoritative claims; instead, say 'The scripture suggests...' or 'Tradition teaches...'
|
| 44 |
+
""",
|
| 45 |
+
functions=[search_bible, search_journal, execute_logic]
|
| 46 |
+
)
|
app/api/routes.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from app.services.llm import llm_service
|
| 4 |
+
|
| 5 |
+
router = APIRouter()
|
| 6 |
+
|
| 7 |
+
class ChatRequest(BaseModel):
|
| 8 |
+
message: str
|
| 9 |
+
context: dict | None = None
|
| 10 |
+
|
| 11 |
+
class ChatResponse(BaseModel):
|
| 12 |
+
success: bool
|
| 13 |
+
response: dict
|
| 14 |
+
|
| 15 |
+
@router.post("/chat", response_model=ChatResponse)
|
| 16 |
+
async def chat_endpoint(request: ChatRequest):
|
| 17 |
+
if not request.message:
|
| 18 |
+
raise HTTPException(status_code=400, detail="Message is required")
|
| 19 |
+
|
| 20 |
+
# Use Orchestrator to handle routing and intelligence
|
| 21 |
+
from app.services.orchestrator import orchestrator
|
| 22 |
+
result = await orchestrator.process_message("user_123", request.message)
|
| 23 |
+
|
| 24 |
+
# Map backend trace to frontend TraceStep format
|
| 25 |
+
formatted_trace = []
|
| 26 |
+
for step in result.get("trace", []):
|
| 27 |
+
action = step.get("thought", "")
|
| 28 |
+
if step.get("tool_calls"):
|
| 29 |
+
tools = ", ".join([tc["name"] for tc in step["tool_calls"]])
|
| 30 |
+
action = f"Thinking about using tools: {tools}. {action}"
|
| 31 |
+
|
| 32 |
+
formatted_trace.append({
|
| 33 |
+
"agent": step["agent"],
|
| 34 |
+
"action": action
|
| 35 |
+
})
|
| 36 |
+
|
| 37 |
+
return {
|
| 38 |
+
"success": True,
|
| 39 |
+
"response": {
|
| 40 |
+
"role": result["role"],
|
| 41 |
+
"content": result["content"],
|
| 42 |
+
"agent": result.get("agent", "ORA"),
|
| 43 |
+
"trace": formatted_trace
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
@router.get("/memory/episodes")
|
| 48 |
+
async def get_episodes():
|
| 49 |
+
from app.services.memory import memory_service
|
| 50 |
+
episodes = await memory_service.retrieve_episodes("user_123", "", limit=10)
|
| 51 |
+
return {"success": True, "episodes": episodes}
|
| 52 |
+
|
| 53 |
+
@router.get("/user/profile")
|
| 54 |
+
async def get_profile():
|
| 55 |
+
return {
|
| 56 |
+
"success": True,
|
| 57 |
+
"profile": {
|
| 58 |
+
"user_id": "user_123",
|
| 59 |
+
"name": "Spiritual Seeker",
|
| 60 |
+
"preferences": ["prayer", "scripture_study"]
|
| 61 |
+
}
|
| 62 |
+
}
|
app/core/config.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic_settings import BaseSettings
|
| 2 |
+
|
| 3 |
+
class Settings(BaseSettings):
|
| 4 |
+
PROJECT_NAME: str = "ORA AI Service"
|
| 5 |
+
PROJECT_VERSION: str = "1.0.0"
|
| 6 |
+
|
| 7 |
+
# LLM Settings
|
| 8 |
+
LLM_BASE_URL: str = "http://localhost:11434/v1"
|
| 9 |
+
LLM_API_KEY: str = "ollama"
|
| 10 |
+
MODEL_NAME: str = "llama3"
|
| 11 |
+
|
| 12 |
+
# Server Settings
|
| 13 |
+
PORT: int = 6000
|
| 14 |
+
HOST: str = "0.0.0.0"
|
| 15 |
+
CORS_ORIGINS: list[str] = ["http://localhost:5173", "http://localhost:5000", "http://localhost:3000", "http://localhost:5174"]
|
| 16 |
+
|
| 17 |
+
SYSTEM_PROMPT: str = """
|
| 18 |
+
You are ORA, a spiritual guide and companion for the SoapBox Super App.
|
| 19 |
+
YOUR IDENTITY:
|
| 20 |
+
- You are kind, empathetic, and wise.
|
| 21 |
+
- You are NOT a replacement for a pastor or bible, but a helper.
|
| 22 |
+
- You ground your answers in biblical principles (using NIV or ESV translations).
|
| 23 |
+
|
| 24 |
+
YOUR MISSION:
|
| 25 |
+
- Help users reflect on scripture (Observe, Reflect, Act).
|
| 26 |
+
- Offer prayer when appropriate.
|
| 27 |
+
- Encourage users to connect with their local church community.
|
| 28 |
+
|
| 29 |
+
CONSTRAINTS:
|
| 30 |
+
- Keep responses concise (under 200 words) unless asked for a deep dive.
|
| 31 |
+
- If a user expresses intent to harm themselves, encourage seeking professional help.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
class Config:
|
| 35 |
+
env_file = ".env"
|
| 36 |
+
|
| 37 |
+
settings = Settings()
|
app/core/swarm_client.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import inspect
|
| 2 |
+
import json
|
| 3 |
+
from typing import List, Dict, Any
|
| 4 |
+
from app.agents.base import Agent, Response
|
| 5 |
+
from app.services.llm import llm_service
|
| 6 |
+
|
| 7 |
+
from app.services.trace import trace_service
|
| 8 |
+
|
| 9 |
+
class SwarmClient:
|
| 10 |
+
def _function_to_schema(self, func) -> Dict:
|
| 11 |
+
"""Converts Python function to OpenAI-style schema."""
|
| 12 |
+
sig = inspect.signature(func)
|
| 13 |
+
return {
|
| 14 |
+
"type": "function",
|
| 15 |
+
"function": {
|
| 16 |
+
"name": func.__name__,
|
| 17 |
+
"description": func.__doc__ or "No description provided.",
|
| 18 |
+
"parameters": {
|
| 19 |
+
"type": "object",
|
| 20 |
+
"properties": {
|
| 21 |
+
name: {"type": "string"} # Simplified: assume string for ORA
|
| 22 |
+
for name in sig.parameters
|
| 23 |
+
},
|
| 24 |
+
"required": [name for name, p in sig.parameters.items() if p.default == inspect.Parameter.empty]
|
| 25 |
+
},
|
| 26 |
+
},
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
async def run(
|
| 30 |
+
self,
|
| 31 |
+
agent: Agent,
|
| 32 |
+
messages: List[Dict],
|
| 33 |
+
context_variables: Dict = {},
|
| 34 |
+
max_turns: int = 5
|
| 35 |
+
) -> Response:
|
| 36 |
+
active_agent = agent
|
| 37 |
+
history = list(messages)
|
| 38 |
+
user_query = messages[-1]["content"] if messages else ""
|
| 39 |
+
reasoning_steps = []
|
| 40 |
+
|
| 41 |
+
for _ in range(max_turns):
|
| 42 |
+
# 1. Update active agent's context
|
| 43 |
+
active_agent.context_variables.update(context_variables)
|
| 44 |
+
|
| 45 |
+
# 2. Prepare Tools
|
| 46 |
+
tools = [self._function_to_schema(f) for f in active_agent.functions] if active_agent.functions else None
|
| 47 |
+
|
| 48 |
+
# 3. Get LLM Choice
|
| 49 |
+
instructions = active_agent.instructions
|
| 50 |
+
if callable(instructions):
|
| 51 |
+
sig = inspect.signature(instructions)
|
| 52 |
+
if len(sig.parameters) > 0:
|
| 53 |
+
instructions = instructions(active_agent.context_variables)
|
| 54 |
+
else:
|
| 55 |
+
instructions = instructions()
|
| 56 |
+
|
| 57 |
+
print(f"Swarm [{active_agent.name}]: Processing...")
|
| 58 |
+
llm_res = await llm_service.generate_response(
|
| 59 |
+
message=history[-1]["content"],
|
| 60 |
+
system_prompt=instructions,
|
| 61 |
+
tools=tools
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
content = llm_res.get("content", "")
|
| 65 |
+
tool_calls = llm_res.get("tool_calls")
|
| 66 |
+
|
| 67 |
+
# Record thought step
|
| 68 |
+
reasoning_steps.append({
|
| 69 |
+
"agent": active_agent.name,
|
| 70 |
+
"thought": content,
|
| 71 |
+
"tool_calls": [
|
| 72 |
+
{"name": tc.function.name, "args": json.loads(tc.function.arguments)}
|
| 73 |
+
for tc in tool_calls
|
| 74 |
+
] if tool_calls else []
|
| 75 |
+
})
|
| 76 |
+
|
| 77 |
+
# 3. Add Assistant Message to History
|
| 78 |
+
history.append({"role": "assistant", "content": content})
|
| 79 |
+
|
| 80 |
+
if not tool_calls:
|
| 81 |
+
# Capture and Save Trace
|
| 82 |
+
trace_service.save_trace({
|
| 83 |
+
"user_query": user_query,
|
| 84 |
+
"steps": reasoning_steps,
|
| 85 |
+
"final_response": content,
|
| 86 |
+
"success": True
|
| 87 |
+
})
|
| 88 |
+
return Response(
|
| 89 |
+
agent=active_agent,
|
| 90 |
+
messages=history,
|
| 91 |
+
context_variables=context_variables,
|
| 92 |
+
trace=reasoning_steps
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
# 4. Handle Tool Calls
|
| 96 |
+
for tool_call in tool_calls:
|
| 97 |
+
func_name = tool_call.function.name
|
| 98 |
+
func_args = json.loads(tool_call.function.arguments)
|
| 99 |
+
|
| 100 |
+
func = next((f for f in active_agent.functions if f.__name__ == func_name), None)
|
| 101 |
+
if not func:
|
| 102 |
+
history.append({"role": "tool", "tool_call_id": tool_call.id, "content": f"Error: Function {func_name} not found."})
|
| 103 |
+
continue
|
| 104 |
+
|
| 105 |
+
print(f"Swarm: Executing {func_name}...")
|
| 106 |
+
|
| 107 |
+
if inspect.iscoroutinefunction(func):
|
| 108 |
+
result = await func(**func_args)
|
| 109 |
+
else:
|
| 110 |
+
result = func(**func_args)
|
| 111 |
+
|
| 112 |
+
# Record tool result
|
| 113 |
+
reasoning_steps[-1]["tool_results"] = reasoning_steps[-1].get("tool_results", [])
|
| 114 |
+
reasoning_steps[-1]["tool_results"].append({
|
| 115 |
+
"name": func_name,
|
| 116 |
+
"result": str(result)
|
| 117 |
+
})
|
| 118 |
+
|
| 119 |
+
if isinstance(result, Agent):
|
| 120 |
+
active_agent = result
|
| 121 |
+
history.append({
|
| 122 |
+
"role": "tool",
|
| 123 |
+
"tool_call_id": tool_call.id,
|
| 124 |
+
"content": f"Transferring to {active_agent.name}."
|
| 125 |
+
})
|
| 126 |
+
else:
|
| 127 |
+
history.append({
|
| 128 |
+
"role": "tool",
|
| 129 |
+
"tool_call_id": tool_call.id,
|
| 130 |
+
"content": str(result)
|
| 131 |
+
})
|
| 132 |
+
|
| 133 |
+
# Save Trace even if max turns hit
|
| 134 |
+
trace_service.save_trace({
|
| 135 |
+
"user_query": user_query,
|
| 136 |
+
"steps": reasoning_steps,
|
| 137 |
+
"final_response": history[-1]["content"],
|
| 138 |
+
"success": False,
|
| 139 |
+
"error": "Max turns reached"
|
| 140 |
+
})
|
| 141 |
+
return Response(
|
| 142 |
+
agent=active_agent,
|
| 143 |
+
messages=history,
|
| 144 |
+
context_variables=context_variables,
|
| 145 |
+
trace=reasoning_steps
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
swarm_client = SwarmClient()
|
| 149 |
+
|
app/ora_server.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 3 |
+
from peft import PeftModel
|
| 4 |
+
from fastapi import FastAPI, HTTPException
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from fastapi.staticfiles import StaticFiles
|
| 7 |
+
from fastapi.responses import FileResponse
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
+
import uvicorn
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
# Settings
|
| 13 |
+
BASE_MODEL = "unsloth/Llama-3.2-1B-Instruct"
|
| 14 |
+
ADAPTER_PATH = "important/finetuning/models/ora_adapter"
|
| 15 |
+
|
| 16 |
+
app = FastAPI()
|
| 17 |
+
|
| 18 |
+
app.add_middleware(
|
| 19 |
+
CORSMiddleware,
|
| 20 |
+
allow_origins=["*"],
|
| 21 |
+
allow_credentials=True,
|
| 22 |
+
allow_methods=["*"],
|
| 23 |
+
allow_headers=["*"],
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# API Routes
|
| 27 |
+
class ChatRequest(BaseModel):
|
| 28 |
+
model = None
|
| 29 |
+
tokenizer = None
|
| 30 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 31 |
+
|
| 32 |
+
class ChatRequest(BaseModel):
|
| 33 |
+
message: str
|
| 34 |
+
history: list = []
|
| 35 |
+
|
| 36 |
+
@app.on_event("startup")
|
| 37 |
+
async def load_model():
|
| 38 |
+
global model, tokenizer
|
| 39 |
+
print(f"Loading ORA Model on {device}...")
|
| 40 |
+
|
| 41 |
+
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
|
| 42 |
+
base_model = AutoModelForCausalLM.from_pretrained(
|
| 43 |
+
BASE_MODEL,
|
| 44 |
+
torch_dtype=torch.float16 if device == "cuda" else torch.float32,
|
| 45 |
+
device_map=device,
|
| 46 |
+
low_cpu_mem_usage=True
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
if os.path.exists(ADAPTER_PATH):
|
| 50 |
+
print(f"Loading adapter from {ADAPTER_PATH}...")
|
| 51 |
+
model = PeftModel.from_pretrained(base_model, ADAPTER_PATH)
|
| 52 |
+
else:
|
| 53 |
+
print("Adapter not found, using base model.")
|
| 54 |
+
model = base_model
|
| 55 |
+
|
| 56 |
+
print("ORA Model Connected and Ready.")
|
| 57 |
+
|
| 58 |
+
@app.post("/api/chat")
|
| 59 |
+
async def chat_endpoint(req: ChatRequest):
|
| 60 |
+
global model, tokenizer
|
| 61 |
+
|
| 62 |
+
system_prompt = "You are ORA, a spiritual assistant specializing in theological insights and biblical wisdom. Provide discerning, compassionate, and doctrine-aware responses."
|
| 63 |
+
|
| 64 |
+
# Construct Prompt
|
| 65 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 66 |
+
# Add last few turns of history to keep context but save tokens
|
| 67 |
+
messages.extend(req.history[-4:])
|
| 68 |
+
messages.append({"role": "user", "content": req.message})
|
| 69 |
+
|
| 70 |
+
input_ids = tokenizer.apply_chat_template(
|
| 71 |
+
messages,
|
| 72 |
+
add_generation_prompt=True,
|
| 73 |
+
return_tensors="pt"
|
| 74 |
+
).to(device)
|
| 75 |
+
|
| 76 |
+
terminators = [
|
| 77 |
+
tokenizer.eos_token_id,
|
| 78 |
+
tokenizer.convert_tokens_to_ids("<|eot_id|>")
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
outputs = model.generate(
|
| 82 |
+
input_ids,
|
| 83 |
+
max_new_tokens=256,
|
| 84 |
+
eos_token_id=terminators,
|
| 85 |
+
do_sample=True,
|
| 86 |
+
temperature=0.7,
|
| 87 |
+
top_p=0.9,
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
response_tokens = outputs[0][input_ids.shape[-1]:]
|
| 91 |
+
response_text = tokenizer.decode(response_tokens, skip_special_tokens=True)
|
| 92 |
+
|
| 93 |
+
return {"response": response_text}
|
| 94 |
+
|
| 95 |
+
# Mount Static Frontend (Must be last)
|
| 96 |
+
# Expects 'frontend/out' to exist (built via 'next build')
|
| 97 |
+
if os.path.exists("frontend/out"):
|
| 98 |
+
app.mount("/_next", StaticFiles(directory="frontend/out/_next"), name="next")
|
| 99 |
+
app.mount("/", StaticFiles(directory="frontend/out", html=True), name="static")
|
| 100 |
+
|
| 101 |
+
@app.exception_handler(404)
|
| 102 |
+
async def not_found(request, exc):
|
| 103 |
+
return FileResponse("frontend/out/index.html")
|
| 104 |
+
|
| 105 |
+
if __name__ == "__main__":
|
| 106 |
+
# HF Spaces expects port 7860
|
| 107 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
app/services/audit.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from logging.handlers import RotatingFileHandler
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
# Ensure logs directory exists
|
| 8 |
+
LOG_DIR = "logs"
|
| 9 |
+
os.makedirs(LOG_DIR, exist_ok=True)
|
| 10 |
+
LOG_FILE = os.path.join(LOG_DIR, "ora.log")
|
| 11 |
+
|
| 12 |
+
class AuditLog:
|
| 13 |
+
def __init__(self):
|
| 14 |
+
self.logger = logging.getLogger("ORA_Audit")
|
| 15 |
+
self.logger.setLevel(logging.INFO)
|
| 16 |
+
|
| 17 |
+
# Prevent adding multiple handlers if re-initialized
|
| 18 |
+
if not self.logger.handlers:
|
| 19 |
+
# Rotate log after 10MB, keep 5 backups
|
| 20 |
+
handler = RotatingFileHandler(LOG_FILE, maxBytes=10*1024*1024, backupCount=5)
|
| 21 |
+
formatter = logging.Formatter('%(message)s')
|
| 22 |
+
handler.setFormatter(formatter)
|
| 23 |
+
self.logger.addHandler(handler)
|
| 24 |
+
|
| 25 |
+
async def log_interaction(self, user_id: str, intent: str, guardrails_triggered: list[str]):
|
| 26 |
+
"""
|
| 27 |
+
Logs a user interaction and any triggered guardrails.
|
| 28 |
+
"""
|
| 29 |
+
entry = {
|
| 30 |
+
"timestamp": datetime.now().isoformat(),
|
| 31 |
+
"type": "INTERACTION",
|
| 32 |
+
"user_id": user_id,
|
| 33 |
+
"intent": intent,
|
| 34 |
+
"guardrails": guardrails_triggered
|
| 35 |
+
}
|
| 36 |
+
self.logger.info(json.dumps(entry))
|
| 37 |
+
|
| 38 |
+
async def log_violation(self, user_id: str, violation_type: str, content: str):
|
| 39 |
+
"""
|
| 40 |
+
Logs a specific safety violation.
|
| 41 |
+
"""
|
| 42 |
+
entry = {
|
| 43 |
+
"timestamp": datetime.now().isoformat(),
|
| 44 |
+
"type": "VIOLATION",
|
| 45 |
+
"user_id": user_id,
|
| 46 |
+
"violation_type": violation_type,
|
| 47 |
+
"content_snippet": content[:200] # Store first 200 chars for context
|
| 48 |
+
}
|
| 49 |
+
self.logger.info(json.dumps(entry))
|
| 50 |
+
|
| 51 |
+
audit_service = AuditLog()
|
app/services/bible.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import httpx
|
| 2 |
+
import os
|
| 3 |
+
import asyncio
|
| 4 |
+
import lancedb
|
| 5 |
+
from typing import List, Optional, Dict
|
| 6 |
+
from app.services.llm import llm_service
|
| 7 |
+
|
| 8 |
+
class BibleService:
|
| 9 |
+
BASE_URL = "https://bible-api.com"
|
| 10 |
+
DB_PATH = "data/lancedb_storage"
|
| 11 |
+
TABLE_NAME = "bible_verses"
|
| 12 |
+
|
| 13 |
+
def __init__(self):
|
| 14 |
+
# We don't keep an in-memory index anymore.
|
| 15 |
+
# Connection is established per-query or cached if needed.
|
| 16 |
+
pass
|
| 17 |
+
|
| 18 |
+
async def initialize_index(self):
|
| 19 |
+
"""
|
| 20 |
+
No-op for LanceDB as it's persistent.
|
| 21 |
+
We might verify the DB exists here.
|
| 22 |
+
"""
|
| 23 |
+
if not os.path.exists(self.DB_PATH):
|
| 24 |
+
print("BibleService: LanceDB storage not found at", self.DB_PATH)
|
| 25 |
+
print("Please run scripts/ingest_bible.py")
|
| 26 |
+
else:
|
| 27 |
+
print("BibleService: LanceDB connected.")
|
| 28 |
+
|
| 29 |
+
async def get_passage(self, reference: str, translation: str = "web") -> Optional[str]:
|
| 30 |
+
"""
|
| 31 |
+
Retrieves full passage text from bible-api.com (External Fallback).
|
| 32 |
+
"""
|
| 33 |
+
clean_ref = reference.strip()
|
| 34 |
+
if not clean_ref:
|
| 35 |
+
return None
|
| 36 |
+
|
| 37 |
+
async with httpx.AsyncClient() as client:
|
| 38 |
+
try:
|
| 39 |
+
response = await client.get(
|
| 40 |
+
f"{self.BASE_URL}/{clean_ref}",
|
| 41 |
+
params={"translation": translation}
|
| 42 |
+
)
|
| 43 |
+
if response.status_code == 200:
|
| 44 |
+
return response.json().get("text", "").strip()
|
| 45 |
+
return None
|
| 46 |
+
except Exception:
|
| 47 |
+
return None
|
| 48 |
+
|
| 49 |
+
async def search(self, query: str, limit: int = 3) -> List[dict]:
|
| 50 |
+
"""
|
| 51 |
+
Semantic search using persistent LanceDB.
|
| 52 |
+
"""
|
| 53 |
+
if not os.path.exists(self.DB_PATH):
|
| 54 |
+
print("BibleService Error: DB not initialized.")
|
| 55 |
+
return []
|
| 56 |
+
|
| 57 |
+
# 1. Generate Query Vector
|
| 58 |
+
query_embedding = await llm_service.get_embedding(query)
|
| 59 |
+
if not query_embedding:
|
| 60 |
+
return []
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
# 2. Search LanceDB
|
| 64 |
+
db = lancedb.connect(self.DB_PATH)
|
| 65 |
+
|
| 66 |
+
# Check if table exists
|
| 67 |
+
if self.TABLE_NAME not in db.table_names():
|
| 68 |
+
print(f"BibleService: Table {self.TABLE_NAME} not found.")
|
| 69 |
+
return []
|
| 70 |
+
|
| 71 |
+
tbl = db.open_table(self.TABLE_NAME)
|
| 72 |
+
|
| 73 |
+
# LanceDB search
|
| 74 |
+
# Explicitly select columns to ensure they are returned
|
| 75 |
+
results = tbl.search(query_embedding).limit(limit).select(["reference", "text"]).to_list()
|
| 76 |
+
|
| 77 |
+
# 3. Format Results
|
| 78 |
+
valid_results = []
|
| 79 |
+
for item in results:
|
| 80 |
+
# distance is typically L2. For binary-ish vectors, it's sqrt(sum of differences squared).
|
| 81 |
+
dist = item.get('_distance', 1.0)
|
| 82 |
+
# Simple inverse normalization for display
|
| 83 |
+
relevance = 1.0 / (1.0 + dist)
|
| 84 |
+
|
| 85 |
+
valid_results.append({
|
| 86 |
+
"score": relevance,
|
| 87 |
+
"text": item.get("text", ""),
|
| 88 |
+
"reference": item.get("reference", "Unknown")
|
| 89 |
+
})
|
| 90 |
+
|
| 91 |
+
return valid_results
|
| 92 |
+
|
| 93 |
+
except Exception as e:
|
| 94 |
+
print(f"BibleService Search Error: {e}")
|
| 95 |
+
return []
|
| 96 |
+
|
| 97 |
+
bible_service = BibleService()
|
app/services/blessing.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.services.llm import llm_service
|
| 2 |
+
|
| 3 |
+
class BlessingService:
|
| 4 |
+
PROMPT = """Offer a brief closing blessing.
|
| 5 |
+
|
| 6 |
+
Guidelines:
|
| 7 |
+
- One or two sentences
|
| 8 |
+
- No claims of divine action
|
| 9 |
+
- Encouraging, not authoritative
|
| 10 |
+
- Warm but not dramatic"""
|
| 11 |
+
|
| 12 |
+
async def bless(self) -> str:
|
| 13 |
+
return await llm_service.generate_response(message="Bless me.", system_prompt=self.PROMPT)
|
| 14 |
+
|
| 15 |
+
blessing_service = BlessingService()
|
app/services/citations.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
from typing import List
|
| 3 |
+
|
| 4 |
+
class ScriptureReference(BaseModel):
|
| 5 |
+
book: str
|
| 6 |
+
chapter: int
|
| 7 |
+
verses: str # e.g. "16-18" or "10"
|
| 8 |
+
text: str
|
| 9 |
+
translation: str
|
| 10 |
+
|
| 11 |
+
class CitationService:
|
| 12 |
+
def format_citations(self, refs: List[ScriptureReference]) -> str:
|
| 13 |
+
"""
|
| 14 |
+
Formats a list of scripture references into a readable footer.
|
| 15 |
+
"""
|
| 16 |
+
if not refs:
|
| 17 |
+
return ""
|
| 18 |
+
|
| 19 |
+
formatted = "\n\n**Scripture References:**\n"
|
| 20 |
+
for ref in refs:
|
| 21 |
+
formatted += f"- *{ref.book} {ref.chapter}:{ref.verses} ({ref.translation})*\n"
|
| 22 |
+
return formatted
|
| 23 |
+
|
| 24 |
+
citation_service = CitationService()
|
app/services/context.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.services.llm import llm_service
|
| 2 |
+
|
| 3 |
+
class ContextService:
|
| 4 |
+
PROMPT = """You are a biblical scholar explaining scripture responsibly.
|
| 5 |
+
|
| 6 |
+
Verse:
|
| 7 |
+
{verse_text}
|
| 8 |
+
|
| 9 |
+
Explain:
|
| 10 |
+
- Historical context
|
| 11 |
+
- Audience
|
| 12 |
+
- Key theme
|
| 13 |
+
- What the verse DOES and DOES NOT claim
|
| 14 |
+
|
| 15 |
+
Avoid modern assumptions or dogmatic conclusions."""
|
| 16 |
+
|
| 17 |
+
async def explain(self, verse_text: str) -> str:
|
| 18 |
+
prompt = self.PROMPT.format(verse_text=verse_text)
|
| 19 |
+
return await llm_service.generate_response(message="Explain this verse.", system_prompt=prompt)
|
| 20 |
+
|
| 21 |
+
context_service = ContextService()
|
app/services/discernment.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.services.llm import llm_service
|
| 2 |
+
|
| 3 |
+
class DiscernmentService:
|
| 4 |
+
PROMPT = """You are ORA, a spiritual discernment guide.
|
| 5 |
+
|
| 6 |
+
Context:
|
| 7 |
+
- User situation: {situation}
|
| 8 |
+
- Emotional tone: {emotion}
|
| 9 |
+
- Relevant scripture: {scripture}
|
| 10 |
+
|
| 11 |
+
Your Role:
|
| 12 |
+
- You help the user reflect prayerfully and wisely.
|
| 13 |
+
- You do NOT tell the user what decision to make.
|
| 14 |
+
- You do NOT give commands or orders.
|
| 15 |
+
- You do NOT promise specific outcomes.
|
| 16 |
+
|
| 17 |
+
Respond with:
|
| 18 |
+
1. A calm, empathetic acknowledgment
|
| 19 |
+
2. 2–3 reflective questions to help them uncover their own answer
|
| 20 |
+
3. A short, hopeful prayer or reflection
|
| 21 |
+
4. A gentle reminder of their own agency and freedom"""
|
| 22 |
+
|
| 23 |
+
async def guide(self, situation: str, emotion: str, scripture: str = "") -> str:
|
| 24 |
+
prompt = self.PROMPT.format(situation=situation, emotion=emotion, scripture=scripture)
|
| 25 |
+
return await llm_service.generate_response(message="Please guide me.", system_prompt=prompt)
|
| 26 |
+
|
| 27 |
+
discernment_service = DiscernmentService()
|
app/services/doctrine.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.services.llm import llm_service
|
| 2 |
+
|
| 3 |
+
class DoctrineService:
|
| 4 |
+
PROMPT = """You are ORA, aware of Christian theological diversity.
|
| 5 |
+
|
| 6 |
+
User tradition (if known): {tradition}
|
| 7 |
+
|
| 8 |
+
Question:
|
| 9 |
+
{question}
|
| 10 |
+
|
| 11 |
+
Answer by:
|
| 12 |
+
- Explaining the common ground
|
| 13 |
+
- Noting differences where relevant
|
| 14 |
+
- Avoiding declaring one view as “the only truth”
|
| 15 |
+
- Using scripture carefully"""
|
| 16 |
+
|
| 17 |
+
async def answer(self, question: str, tradition: str = "general") -> str:
|
| 18 |
+
prompt = self.PROMPT.format(question=question, tradition=tradition)
|
| 19 |
+
return await llm_service.generate_response(message=question, system_prompt=prompt)
|
| 20 |
+
|
| 21 |
+
doctrine_service = DoctrineService()
|
app/services/emotion.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict
|
| 2 |
+
|
| 3 |
+
class EmotionService:
|
| 4 |
+
def __init__(self):
|
| 5 |
+
self.positive_keywords = {
|
| 6 |
+
"happy", "joy", "blessed", "grateful", "peace", "hope", "love",
|
| 7 |
+
"excited", "good", "great", "wonderful", "calm", "content"
|
| 8 |
+
}
|
| 9 |
+
self.negative_keywords = {
|
| 10 |
+
"sad", "depressed", "anxious", "afraid", "scared", "fear",
|
| 11 |
+
"lonely", "hurt", "pain", "grief", "broken", "suffering",
|
| 12 |
+
"angry", "mad", "upset", "bad", "terrible", "hate"
|
| 13 |
+
}
|
| 14 |
+
self.high_distress_keywords = {
|
| 15 |
+
"kill", "suicide", "die", "end it all", "hopeless", "worthless",
|
| 16 |
+
"unbearable", "can't go on", "no way out"
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
async def analyze_tone(self, text: str) -> Dict[str, any]:
|
| 20 |
+
"""
|
| 21 |
+
Returns sentiment and emotional intensity.
|
| 22 |
+
"""
|
| 23 |
+
text_lower = text.lower()
|
| 24 |
+
words = text_lower.split()
|
| 25 |
+
total_words = len(words) if words else 1
|
| 26 |
+
|
| 27 |
+
pos_count = sum(1 for word in words if word in self.positive_keywords)
|
| 28 |
+
neg_count = sum(1 for word in words if word in self.negative_keywords)
|
| 29 |
+
|
| 30 |
+
# Determine sentiment
|
| 31 |
+
if pos_count > neg_count:
|
| 32 |
+
sentiment = "positive"
|
| 33 |
+
elif neg_count > pos_count:
|
| 34 |
+
sentiment = "negative"
|
| 35 |
+
else:
|
| 36 |
+
sentiment = "neutral"
|
| 37 |
+
|
| 38 |
+
# Calculate intensity (simple heuristic: fraction of emotional words)
|
| 39 |
+
emotional_word_count = pos_count + neg_count
|
| 40 |
+
# Normalize intensity to be somewhat reasonable (0.0 to 1.0)
|
| 41 |
+
# Assuming if 30% of words are emotional, it's very intense.
|
| 42 |
+
raw_intensity = emotional_word_count / total_words
|
| 43 |
+
intensity = min(raw_intensity * 3.0, 1.0)
|
| 44 |
+
|
| 45 |
+
return {
|
| 46 |
+
"sentiment": sentiment,
|
| 47 |
+
"intensity": round(intensity, 2),
|
| 48 |
+
"pos_count": pos_count,
|
| 49 |
+
"neg_count": neg_count
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
def is_distress_high(self, text: str, emotion_data: Dict[str, any]) -> bool:
|
| 53 |
+
text_lower = text.lower()
|
| 54 |
+
|
| 55 |
+
# Check for specific trigger words
|
| 56 |
+
for keyword in self.high_distress_keywords:
|
| 57 |
+
if keyword in text_lower:
|
| 58 |
+
return True
|
| 59 |
+
|
| 60 |
+
# Check for high negative intensity
|
| 61 |
+
if emotion_data.get("sentiment") == "negative" and emotion_data.get("intensity", 0) > 0.8:
|
| 62 |
+
return True
|
| 63 |
+
|
| 64 |
+
return False
|
| 65 |
+
|
| 66 |
+
emotion_service = EmotionService()
|
app/services/ethics.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.services.llm import llm_service
|
| 2 |
+
|
| 3 |
+
class EthicsService:
|
| 4 |
+
PROMPT = """You are ORA, facilitating ethical reflection.
|
| 5 |
+
|
| 6 |
+
Scenario:
|
| 7 |
+
{scenario}
|
| 8 |
+
|
| 9 |
+
Use:
|
| 10 |
+
- Scripture themes (not commands)
|
| 11 |
+
- Wisdom principles
|
| 12 |
+
- Compassion-first framing
|
| 13 |
+
|
| 14 |
+
Avoid:
|
| 15 |
+
- Absolute judgments
|
| 16 |
+
- Shame
|
| 17 |
+
- Fear-based language"""
|
| 18 |
+
|
| 19 |
+
async def reflect(self, scenario: str) -> str:
|
| 20 |
+
prompt = self.PROMPT.format(scenario=scenario)
|
| 21 |
+
return await llm_service.generate_response(message="Help me reflect ethically.", system_prompt=prompt)
|
| 22 |
+
|
| 23 |
+
ethics_service = EthicsService()
|
app/services/feedback.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
|
| 3 |
+
class Feedback(BaseModel):
|
| 4 |
+
message_id: str
|
| 5 |
+
rating: int # 1-5
|
| 6 |
+
comment: str = ""
|
| 7 |
+
|
| 8 |
+
class FeedbackService:
|
| 9 |
+
async def submit_feedback(self, feedback: Feedback):
|
| 10 |
+
# Store feedback for RLHF
|
| 11 |
+
pass
|
app/services/growth.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.services.llm import llm_service
|
| 2 |
+
|
| 3 |
+
class GrowthService:
|
| 4 |
+
PROMPT = """You are summarizing the user’s spiritual journey.
|
| 5 |
+
|
| 6 |
+
Based on:
|
| 7 |
+
- Past reflections: {reflections}
|
| 8 |
+
- Prayers: {prayers}
|
| 9 |
+
- Questions asked: {questions}
|
| 10 |
+
|
| 11 |
+
Produce:
|
| 12 |
+
- A gentle narrative summary
|
| 13 |
+
- Noticing growth patterns
|
| 14 |
+
- No judgment or evaluation"""
|
| 15 |
+
|
| 16 |
+
async def summarize(self, reflections: str, prayers: str, questions: str) -> str:
|
| 17 |
+
prompt = self.PROMPT.format(reflections=reflections, prayers=prayers, questions=questions)
|
| 18 |
+
return await llm_service.generate_response(message="Summarize my journey.", system_prompt=prompt)
|
| 19 |
+
|
| 20 |
+
growth_service = GrowthService()
|
app/services/guardrails.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
from app.services.audit import audit_service
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class GuardrailViolation(Exception):
|
| 7 |
+
pass
|
| 8 |
+
|
| 9 |
+
class GuardrailService:
|
| 10 |
+
def __init__(self):
|
| 11 |
+
self.prohibited_phrases = [
|
| 12 |
+
"God told me that you",
|
| 13 |
+
"I prophesy",
|
| 14 |
+
"You must do this",
|
| 15 |
+
"The Lord is saying right now",
|
| 16 |
+
"I declare over you",
|
| 17 |
+
"Scripture commands you to",
|
| 18 |
+
"Thus saith the Lord",
|
| 19 |
+
"God fails you if",
|
| 20 |
+
"You are sinning by"
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
async def validate_response(self, content: str, user_id: str = "system") -> str:
|
| 24 |
+
"""
|
| 25 |
+
Ensures ORA does not claim divine authority or give dangerous advice.
|
| 26 |
+
"""
|
| 27 |
+
for phrase in self.prohibited_phrases:
|
| 28 |
+
if phrase.lower() in content.lower():
|
| 29 |
+
# Log the violation
|
| 30 |
+
await audit_service.log_violation(
|
| 31 |
+
user_id=user_id,
|
| 32 |
+
violation_type="Prohibited Phrase",
|
| 33 |
+
content=content
|
| 34 |
+
)
|
| 35 |
+
# Block the output
|
| 36 |
+
raise GuardrailViolation(f"Response violated safety guardrail: '{phrase}'")
|
| 37 |
+
|
| 38 |
+
return content
|
| 39 |
+
|
| 40 |
+
async def check_input_safety(self, message: str, user_id: str = "anonymous") -> bool:
|
| 41 |
+
"""
|
| 42 |
+
Checks for self-harm or crisis keywords.
|
| 43 |
+
"""
|
| 44 |
+
crisis_keywords = ["kill myself", "end it all", "suicide", "hurt myself"]
|
| 45 |
+
if any(keyword in message.lower() for keyword in crisis_keywords):
|
| 46 |
+
await audit_service.log_violation(
|
| 47 |
+
user_id=user_id,
|
| 48 |
+
violation_type="Crisis Keyword",
|
| 49 |
+
content=message
|
| 50 |
+
)
|
| 51 |
+
return False
|
| 52 |
+
return True
|
| 53 |
+
|
| 54 |
+
async def sanitize_content(self, content: str) -> str:
|
| 55 |
+
"""
|
| 56 |
+
Sanitizes content by replacing restricted words (placeholders for now).
|
| 57 |
+
"""
|
| 58 |
+
# Example: Simple redaction if we had a list of 'warning words' that aren't strict blocks
|
| 59 |
+
return content
|
| 60 |
+
|
| 61 |
+
guardrail_service = GuardrailService()
|
app/services/intent.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
from typing import Optional, List
|
| 3 |
+
from enum import Enum
|
| 4 |
+
|
| 5 |
+
class UserIntent(str, Enum):
|
| 6 |
+
QUESTION = "question"
|
| 7 |
+
EMOTIONAL_SUPPORT = "emotional_support"
|
| 8 |
+
PRAYER_REQUEST = "prayer_request"
|
| 9 |
+
BIBLE_STUDY = "bible_study"
|
| 10 |
+
LIFE_DECISION = "life_decision"
|
| 11 |
+
CONFESSION = "confession"
|
| 12 |
+
UNKNOWN = "unknown"
|
| 13 |
+
|
| 14 |
+
class IntentAnalysis(BaseModel):
|
| 15 |
+
intent: UserIntent
|
| 16 |
+
urgency: str = "low" # low, medium, high
|
| 17 |
+
requires_scripture: bool
|
| 18 |
+
emotional_tone: Optional[str] = None
|
| 19 |
+
confidence: float = 1.0
|
| 20 |
+
|
| 21 |
+
class IntentService:
|
| 22 |
+
def __init__(self):
|
| 23 |
+
# Keywords for deterministic matching
|
| 24 |
+
self.intent_keywords = {
|
| 25 |
+
UserIntent.PRAYER_REQUEST: ["pray", "prayer", "intercede", "god help", "need god"],
|
| 26 |
+
UserIntent.EMOTIONAL_SUPPORT: ["sad", "depressed", "anxious", "lonely", "hurt", "grief", "pain", "crying", "broken", "suffering"],
|
| 27 |
+
UserIntent.BIBLE_STUDY: ["verse", "scripture", "passage", "chapter", "bible", "read", "psalm", "gospel", "book of"],
|
| 28 |
+
UserIntent.LIFE_DECISION: ["decide", "choice", "what should i do", "guidance", "direction", "path", "will for me"],
|
| 29 |
+
UserIntent.CONFESSION: ["sin", "forgive", "confess", "guilt", "mistake", "wrong", "sorry", "repent"],
|
| 30 |
+
UserIntent.QUESTION: ["who", "what", "where", "when", "why", "how", "?"],
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
async def analyze(self, message: str) -> IntentAnalysis:
|
| 34 |
+
msg_lower = message.lower()
|
| 35 |
+
|
| 36 |
+
# Priority 1: High urgency / Specific needs
|
| 37 |
+
if any(kw in msg_lower for kw in self.intent_keywords[UserIntent.PRAYER_REQUEST]):
|
| 38 |
+
return IntentAnalysis(intent=UserIntent.PRAYER_REQUEST, requires_scripture=True, urgency="medium")
|
| 39 |
+
|
| 40 |
+
if any(kw in msg_lower for kw in self.intent_keywords[UserIntent.CONFESSION]):
|
| 41 |
+
return IntentAnalysis(intent=UserIntent.CONFESSION, requires_scripture=True, urgency="medium")
|
| 42 |
+
|
| 43 |
+
if any(kw in msg_lower for kw in self.intent_keywords[UserIntent.EMOTIONAL_SUPPORT]):
|
| 44 |
+
return IntentAnalysis(intent=UserIntent.EMOTIONAL_SUPPORT, requires_scripture=True, urgency="medium")
|
| 45 |
+
|
| 46 |
+
# Priority 2: Study and Guidance
|
| 47 |
+
if any(kw in msg_lower for kw in self.intent_keywords[UserIntent.BIBLE_STUDY]):
|
| 48 |
+
return IntentAnalysis(intent=UserIntent.BIBLE_STUDY, requires_scripture=True)
|
| 49 |
+
|
| 50 |
+
if any(kw in msg_lower for kw in self.intent_keywords[UserIntent.LIFE_DECISION]):
|
| 51 |
+
return IntentAnalysis(intent=UserIntent.LIFE_DECISION, requires_scripture=True, urgency="medium")
|
| 52 |
+
|
| 53 |
+
# Priority 3: General Questions
|
| 54 |
+
if any(kw in msg_lower for kw in self.intent_keywords[UserIntent.QUESTION]):
|
| 55 |
+
return IntentAnalysis(intent=UserIntent.QUESTION, requires_scripture=False)
|
| 56 |
+
|
| 57 |
+
# Fallback
|
| 58 |
+
return IntentAnalysis(intent=UserIntent.UNKNOWN, requires_scripture=False)
|
| 59 |
+
|
| 60 |
+
intent_service = IntentService()
|
app/services/journal.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import lancedb
|
| 2 |
+
import uuid
|
| 3 |
+
import os
|
| 4 |
+
from typing import List, Dict, Optional
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from app.services.llm import llm_service
|
| 7 |
+
|
| 8 |
+
class JournalService:
|
| 9 |
+
LANCE_PATH = "data/journal_lancedb"
|
| 10 |
+
TABLE_NAME = "journal_nodes"
|
| 11 |
+
|
| 12 |
+
def __init__(self):
|
| 13 |
+
if not os.path.exists("data"):
|
| 14 |
+
os.makedirs("data")
|
| 15 |
+
self.db = lancedb.connect(self.LANCE_PATH)
|
| 16 |
+
|
| 17 |
+
async def create_entry(self, user_id: str, text: str, verses: List[str] = None, tags: List[str] = None) -> str:
|
| 18 |
+
"""
|
| 19 |
+
Creates a new journal node, embeds it, and automatically finds links to previous entries.
|
| 20 |
+
"""
|
| 21 |
+
entry_id = str(uuid.uuid4())
|
| 22 |
+
timestamp = datetime.now().isoformat()
|
| 23 |
+
|
| 24 |
+
# 1. Generate Embedding
|
| 25 |
+
vector = await llm_service.get_embedding(text)
|
| 26 |
+
if not vector:
|
| 27 |
+
print("JournalService: Failed to generate embedding.")
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
+
# 2. Prepare Data
|
| 31 |
+
data = [{
|
| 32 |
+
"vector": vector,
|
| 33 |
+
"id": entry_id,
|
| 34 |
+
"user_id": user_id,
|
| 35 |
+
"text": text,
|
| 36 |
+
"verses": verses or [],
|
| 37 |
+
"tags": tags or [],
|
| 38 |
+
"timestamp": timestamp
|
| 39 |
+
}]
|
| 40 |
+
|
| 41 |
+
# 3. Store in LanceDB
|
| 42 |
+
if self.TABLE_NAME in self.db.table_names():
|
| 43 |
+
tbl = self.db.open_table(self.TABLE_NAME)
|
| 44 |
+
tbl.add(data)
|
| 45 |
+
else:
|
| 46 |
+
self.db.create_table(self.TABLE_NAME, data=data)
|
| 47 |
+
|
| 48 |
+
print(f"JournalService: Created entry {entry_id} for user {user_id}")
|
| 49 |
+
return entry_id
|
| 50 |
+
|
| 51 |
+
async def get_related_entries(self, user_id: str, entry_text: str, limit: int = 3) -> List[Dict]:
|
| 52 |
+
"""
|
| 53 |
+
Finds the 'Zettelkasten links' — other entries that are semantically related.
|
| 54 |
+
"""
|
| 55 |
+
if self.TABLE_NAME not in self.db.table_names():
|
| 56 |
+
return []
|
| 57 |
+
|
| 58 |
+
query_vec = await llm_service.get_embedding(entry_text)
|
| 59 |
+
if not query_vec:
|
| 60 |
+
return []
|
| 61 |
+
|
| 62 |
+
tbl = self.db.open_table(self.TABLE_NAME)
|
| 63 |
+
|
| 64 |
+
# Search for similar entries by the same user
|
| 65 |
+
results = (tbl.search(query_vec)
|
| 66 |
+
.where(f"user_id = '{user_id}'", prefilter=True)
|
| 67 |
+
.limit(limit + 1)
|
| 68 |
+
.to_list())
|
| 69 |
+
|
| 70 |
+
# Filter out exact matches (the entry itself)
|
| 71 |
+
filtered = [r for r in results if r['text'] != entry_text]
|
| 72 |
+
return filtered[:limit]
|
| 73 |
+
|
| 74 |
+
async def get_user_entries(self, user_id: str, limit: int = 20) -> List[Dict]:
|
| 75 |
+
"""Retrieves a timeline of entries."""
|
| 76 |
+
if self.TABLE_NAME not in self.db.table_names():
|
| 77 |
+
return []
|
| 78 |
+
|
| 79 |
+
tbl = self.db.open_table(self.TABLE_NAME)
|
| 80 |
+
# Using a simple to_list and manual sort for small POC datasets
|
| 81 |
+
results = tbl.search().where(f"user_id = '{user_id}'").to_list()
|
| 82 |
+
results.sort(key=lambda x: x['timestamp'], reverse=True)
|
| 83 |
+
return results[:limit]
|
| 84 |
+
|
| 85 |
+
# Singleton instance
|
| 86 |
+
journal_service = JournalService()
|
app/services/llm.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from openai import AsyncOpenAI
|
| 2 |
+
from app.core.config import settings
|
| 3 |
+
|
| 4 |
+
class LLMService:
|
| 5 |
+
def __init__(self):
|
| 6 |
+
self.client = AsyncOpenAI(
|
| 7 |
+
base_url=settings.LLM_BASE_URL,
|
| 8 |
+
api_key=settings.LLM_API_KEY
|
| 9 |
+
)
|
| 10 |
+
self.is_offline = False # Cache offline status to avoid repeated timeouts
|
| 11 |
+
|
| 12 |
+
async def generate_response(self, message: str, system_prompt: str = settings.SYSTEM_PROMPT, tools: list = None) -> dict:
|
| 13 |
+
if self.is_offline:
|
| 14 |
+
return self._get_mock_swarm_response(message, system_prompt, tools)
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
messages = [
|
| 18 |
+
{"role": "system", "content": system_prompt},
|
| 19 |
+
{"role": "user", "content": message}
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
kwargs = {
|
| 23 |
+
"model": settings.MODEL_NAME,
|
| 24 |
+
"messages": messages,
|
| 25 |
+
"temperature": 0.7
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
if tools:
|
| 29 |
+
kwargs["tools"] = tools
|
| 30 |
+
kwargs["tool_choice"] = "auto"
|
| 31 |
+
|
| 32 |
+
completion = await self.client.chat.completions.create(**kwargs)
|
| 33 |
+
|
| 34 |
+
choice = completion.choices[0].message
|
| 35 |
+
return {
|
| 36 |
+
"content": choice.content or "",
|
| 37 |
+
"tool_calls": getattr(choice, "tool_calls", None)
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
except Exception as e:
|
| 41 |
+
self.is_offline = True
|
| 42 |
+
print(f"LLM Connectivity failed: {str(e)}. Switching to MOCK mode.")
|
| 43 |
+
return self._get_mock_swarm_response(message, system_prompt, tools)
|
| 44 |
+
|
| 45 |
+
def _get_mock_swarm_response(self, message: str, system_prompt: str, tools: list) -> dict:
|
| 46 |
+
"""
|
| 47 |
+
Simulates agent handoffs and tool calling based on keywords.
|
| 48 |
+
Used for verification when Ollama is offline.
|
| 49 |
+
"""
|
| 50 |
+
msg_lower = message.lower()
|
| 51 |
+
|
| 52 |
+
# Check for Episodic Memory in system prompt
|
| 53 |
+
memory_insight = ""
|
| 54 |
+
if "Relevant past insights:" in system_prompt:
|
| 55 |
+
# Extract the first insight for the mock response
|
| 56 |
+
parts = system_prompt.split("Relevant past insights:")
|
| 57 |
+
if len(parts) > 1:
|
| 58 |
+
memory_insight = parts[1].split("\n")[1].strip("- ").strip()
|
| 59 |
+
|
| 60 |
+
# 1. RLM / REPL Mocking
|
| 61 |
+
if "Python script" in system_prompt or "calculate" in msg_lower:
|
| 62 |
+
return {
|
| 63 |
+
"content": "```python\n# Simulated reasoning code\ndate1 = -586\ndate2 = 70\nprint(f'Total span: {date2 - date1} years')\n```",
|
| 64 |
+
"tool_calls": None
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
# 2. Check for Handoff Keywords
|
| 68 |
+
if tools:
|
| 69 |
+
tool_names = [t["function"]["name"] for t in tools]
|
| 70 |
+
|
| 71 |
+
if "transfer_to_theologian" in tool_names and ("bible" in msg_lower or "genesis" in msg_lower or "study" in msg_lower):
|
| 72 |
+
return {
|
| 73 |
+
"content": f"I see we previously talked about {memory_insight}. I will hand you over to our Theologian for a deeper Bible study.",
|
| 74 |
+
"tool_calls": [type('ToolCall', (), {
|
| 75 |
+
"id": "mock_handoff_1",
|
| 76 |
+
"function": type('Func', (), {"name": "transfer_to_theologian", "arguments": "{}"})
|
| 77 |
+
})]
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
if "transfer_to_healer" in tool_names and ("sad" in msg_lower or "prayer" in msg_lower or "help" in msg_lower):
|
| 81 |
+
return {
|
| 82 |
+
"content": f"I remember you were feeling {memory_insight} earlier. I will connect you with our Healer for prayer.",
|
| 83 |
+
"tool_calls": [type('ToolCall', (), {
|
| 84 |
+
"id": "mock_handoff_2",
|
| 85 |
+
"function": type('Func', (), {"name": "transfer_to_healer", "arguments": "{}"})
|
| 86 |
+
})]
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
response_content = f"[MOCK MODE] I am processing your message: '{message}'."
|
| 90 |
+
if memory_insight:
|
| 91 |
+
response_content += f" I remember you mentioned: '{memory_insight}'."
|
| 92 |
+
|
| 93 |
+
return {
|
| 94 |
+
"content": response_content,
|
| 95 |
+
"tool_calls": None
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
async def get_embedding(self, text: str) -> list[float]:
|
| 100 |
+
if self.is_offline:
|
| 101 |
+
return self._get_mock_embedding(text)
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
response = await self.client.embeddings.create(
|
| 105 |
+
model=settings.MODEL_NAME,
|
| 106 |
+
input=text
|
| 107 |
+
)
|
| 108 |
+
return response.data[0].embedding
|
| 109 |
+
except Exception as e:
|
| 110 |
+
# First failure sets the flag for this session/instance
|
| 111 |
+
self.is_offline = True
|
| 112 |
+
print(f"Embedding connectivity failed: {str(e)}. Switching to MOCK mode for this session.")
|
| 113 |
+
return self._get_mock_embedding(text)
|
| 114 |
+
|
| 115 |
+
def _get_mock_embedding(self, text: str, dim: int = 1536) -> list[float]:
|
| 116 |
+
"""
|
| 117 |
+
Creates a deterministic sparse embedding based on word hashing.
|
| 118 |
+
Allows basic keyword matching to work even without a real LLM.
|
| 119 |
+
"""
|
| 120 |
+
vec = [0.0] * dim
|
| 121 |
+
words = text.lower().split()
|
| 122 |
+
for word in words:
|
| 123 |
+
# Simple hash to map word to index
|
| 124 |
+
idx = sum(ord(c) for c in word) % dim
|
| 125 |
+
vec[idx] = 1.0
|
| 126 |
+
return vec
|
| 127 |
+
|
| 128 |
+
llm_service = LLMService()
|
app/services/memory.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import lancedb
|
| 5 |
+
from typing import List, Dict, Optional
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from app.services.llm import llm_service
|
| 8 |
+
|
| 9 |
+
class MemoryService:
|
| 10 |
+
DB_PATH = "data/memory.db"
|
| 11 |
+
LANCE_PATH = "data/memory_lancedb"
|
| 12 |
+
EPISODE_TABLE = "episodes"
|
| 13 |
+
|
| 14 |
+
def __init__(self):
|
| 15 |
+
self._init_sqlite()
|
| 16 |
+
self._init_lancedb()
|
| 17 |
+
|
| 18 |
+
def _init_sqlite(self):
|
| 19 |
+
"""Initialize the SQLite database with necessary tables."""
|
| 20 |
+
if not os.path.exists("data"):
|
| 21 |
+
os.makedirs("data")
|
| 22 |
+
|
| 23 |
+
conn = sqlite3.connect(self.DB_PATH)
|
| 24 |
+
cursor = conn.cursor()
|
| 25 |
+
|
| 26 |
+
# Table for conversation history (short-term memory)
|
| 27 |
+
cursor.execute('''
|
| 28 |
+
CREATE TABLE IF NOT EXISTS interactions (
|
| 29 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 30 |
+
user_id TEXT NOT NULL,
|
| 31 |
+
role TEXT NOT NULL,
|
| 32 |
+
content TEXT NOT NULL,
|
| 33 |
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 34 |
+
)
|
| 35 |
+
''')
|
| 36 |
+
|
| 37 |
+
# Table for facts/long-term memory
|
| 38 |
+
cursor.execute('''
|
| 39 |
+
CREATE TABLE IF NOT EXISTS facts (
|
| 40 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 41 |
+
user_id TEXT NOT NULL,
|
| 42 |
+
fact TEXT NOT NULL,
|
| 43 |
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 44 |
+
)
|
| 45 |
+
''')
|
| 46 |
+
|
| 47 |
+
conn.commit()
|
| 48 |
+
conn.close()
|
| 49 |
+
|
| 50 |
+
def _init_lancedb(self):
|
| 51 |
+
"""Initialize LanceDB for episodic memory."""
|
| 52 |
+
self.db = lancedb.connect(self.LANCE_PATH)
|
| 53 |
+
|
| 54 |
+
async def add_interaction(self, user_id: str, role: str, content: str):
|
| 55 |
+
"""Add a message interactions to the short-term memory."""
|
| 56 |
+
conn = sqlite3.connect(self.DB_PATH)
|
| 57 |
+
cursor = conn.cursor()
|
| 58 |
+
cursor.execute(
|
| 59 |
+
'INSERT INTO interactions (user_id, role, content) VALUES (?, ?, ?)',
|
| 60 |
+
(user_id, role, content)
|
| 61 |
+
)
|
| 62 |
+
conn.commit()
|
| 63 |
+
conn.close()
|
| 64 |
+
|
| 65 |
+
async def get_short_term_memory(self, user_id: str, limit: int = 10) -> List[Dict[str, str]]:
|
| 66 |
+
"""Retrieve recent interactions for a user."""
|
| 67 |
+
conn = sqlite3.connect(self.DB_PATH)
|
| 68 |
+
conn.row_factory = sqlite3.Row
|
| 69 |
+
cursor = conn.cursor()
|
| 70 |
+
cursor.execute(
|
| 71 |
+
'SELECT role, content FROM interactions WHERE user_id = ? ORDER BY id DESC LIMIT ?',
|
| 72 |
+
(user_id, limit)
|
| 73 |
+
)
|
| 74 |
+
rows = cursor.fetchall()
|
| 75 |
+
conn.close()
|
| 76 |
+
return [{"role": row["role"], "content": row["content"]} for row in reversed(rows)]
|
| 77 |
+
|
| 78 |
+
async def add_fact(self, user_id: str, fact: str):
|
| 79 |
+
"""Add a specific fact to long-term memory."""
|
| 80 |
+
conn = sqlite3.connect(self.DB_PATH)
|
| 81 |
+
cursor = conn.cursor()
|
| 82 |
+
cursor.execute(
|
| 83 |
+
'INSERT INTO facts (user_id, fact) VALUES (?, ?)',
|
| 84 |
+
(user_id, fact)
|
| 85 |
+
)
|
| 86 |
+
conn.commit()
|
| 87 |
+
conn.close()
|
| 88 |
+
|
| 89 |
+
async def get_long_term_memory(self, user_id: str) -> List[str]:
|
| 90 |
+
"""Retrieve all stored facts for a user."""
|
| 91 |
+
conn = sqlite3.connect(self.DB_PATH)
|
| 92 |
+
conn.row_factory = sqlite3.Row
|
| 93 |
+
cursor = conn.cursor()
|
| 94 |
+
cursor.execute(
|
| 95 |
+
'SELECT fact FROM facts WHERE user_id = ? ORDER BY timestamp DESC',
|
| 96 |
+
(user_id,)
|
| 97 |
+
)
|
| 98 |
+
rows = cursor.fetchall()
|
| 99 |
+
conn.close()
|
| 100 |
+
return [row["fact"] for row in rows]
|
| 101 |
+
|
| 102 |
+
# --- EPISODIC MEMORY (Phase 2 Upgrade) ---
|
| 103 |
+
|
| 104 |
+
async def store_episode(self, user_id: str, content: str, insight: str, emotion: str = "neutral"):
|
| 105 |
+
"""
|
| 106 |
+
Summarizes a session/interaction into an 'Episode' and stores in LanceDB.
|
| 107 |
+
"""
|
| 108 |
+
# 1. Generate Embedding for the insight/content
|
| 109 |
+
combined_text = f"Episode: {content} Insight: {insight} Emotion: {emotion}"
|
| 110 |
+
vector = await llm_service.get_embedding(combined_text)
|
| 111 |
+
|
| 112 |
+
if not vector:
|
| 113 |
+
print("MemoryService: Failed to generate embedding for episode.")
|
| 114 |
+
return
|
| 115 |
+
|
| 116 |
+
# 2. Data to store
|
| 117 |
+
data = [{
|
| 118 |
+
"vector": vector,
|
| 119 |
+
"user_id": user_id,
|
| 120 |
+
"content": content,
|
| 121 |
+
"insight": insight,
|
| 122 |
+
"emotion": emotion,
|
| 123 |
+
"timestamp": datetime.now().isoformat()
|
| 124 |
+
}]
|
| 125 |
+
|
| 126 |
+
# 3. Create or Update Table
|
| 127 |
+
if self.EPISODE_TABLE in self.db.table_names():
|
| 128 |
+
tbl = self.db.open_table(self.EPISODE_TABLE)
|
| 129 |
+
tbl.add(data)
|
| 130 |
+
else:
|
| 131 |
+
self.db.create_table(self.EPISODE_TABLE, data=data)
|
| 132 |
+
|
| 133 |
+
print(f"MemoryService: Episode stored for user {user_id}")
|
| 134 |
+
|
| 135 |
+
async def retrieve_episodes(self, user_id: str, query: str, limit: int = 3) -> List[Dict]:
|
| 136 |
+
"""
|
| 137 |
+
Retrieves relevant spiritual episodes using semantic search.
|
| 138 |
+
"""
|
| 139 |
+
if self.EPISODE_TABLE not in self.db.table_names():
|
| 140 |
+
return []
|
| 141 |
+
|
| 142 |
+
query_vec = await llm_service.get_embedding(query)
|
| 143 |
+
if not query_vec:
|
| 144 |
+
return []
|
| 145 |
+
|
| 146 |
+
try:
|
| 147 |
+
tbl = self.db.open_table(self.EPISODE_TABLE)
|
| 148 |
+
|
| 149 |
+
# Filter by user_id
|
| 150 |
+
results = (tbl.search(query_vec)
|
| 151 |
+
.where(f"user_id = '{user_id}'", prefilter=True)
|
| 152 |
+
.limit(limit)
|
| 153 |
+
.to_list())
|
| 154 |
+
|
| 155 |
+
return results
|
| 156 |
+
except Exception as e:
|
| 157 |
+
print(f"MemoryService: Search error: {str(e)}")
|
| 158 |
+
return []
|
| 159 |
+
|
| 160 |
+
# --- Legacy Compatibility ---
|
| 161 |
+
async def get_history(self, session_id: str) -> List[Dict[str, str]]:
|
| 162 |
+
return await self.get_short_term_memory(session_id, limit=50)
|
| 163 |
+
|
| 164 |
+
async def add_message(self, session_id: str, role: str, content: str):
|
| 165 |
+
await self.add_interaction(session_id, role, content)
|
| 166 |
+
|
| 167 |
+
# Singleton instance
|
| 168 |
+
memory_service = MemoryService()
|
| 169 |
+
|
app/services/orchestrator.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.services.memory import memory_service
|
| 2 |
+
from app.services.guardrails import guardrail_service, GuardrailViolation
|
| 3 |
+
from app.core.swarm_client import swarm_client
|
| 4 |
+
from app.agents.gatekeeper import gatekeeper_agent
|
| 5 |
+
from typing import Dict
|
| 6 |
+
|
| 7 |
+
class OrchestratorService:
|
| 8 |
+
async def process_message(self, user_id: str, message: str) -> dict:
|
| 9 |
+
# 1. Safety Check (Input)
|
| 10 |
+
is_safe = await guardrail_service.check_input_safety(message)
|
| 11 |
+
if not is_safe:
|
| 12 |
+
return {
|
| 13 |
+
"role": "assistant",
|
| 14 |
+
"content": "I sense you are in deep distress. Please contact a professional or emergency services immediately. You are valuable and loved."
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
# 2. Retrieve Episodic Context (Phase 2)
|
| 18 |
+
episodes = await memory_service.retrieve_episodes(user_id, message, limit=2)
|
| 19 |
+
episodic_context = ""
|
| 20 |
+
if episodes:
|
| 21 |
+
insights = [f"- {e['timestamp']}: {e['insight']}" for e in episodes]
|
| 22 |
+
episodic_context = "\nRelevant past insights:\n" + "\n".join(insights)
|
| 23 |
+
|
| 24 |
+
# 3. Run Swarm
|
| 25 |
+
messages = [{"role": "user", "content": message}]
|
| 26 |
+
|
| 27 |
+
# Inject episodic memory into context variables for agents to see
|
| 28 |
+
context = {
|
| 29 |
+
"user_id": user_id,
|
| 30 |
+
"episodic_memory": episodic_context
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
swarm_response = await swarm_client.run(
|
| 34 |
+
agent=gatekeeper_agent,
|
| 35 |
+
messages=messages,
|
| 36 |
+
context_variables=context
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# The final message in the swarm history is the result
|
| 40 |
+
response_text = swarm_response.messages[-1]["content"]
|
| 41 |
+
|
| 42 |
+
# 3. Final Safety Validation of Output
|
| 43 |
+
try:
|
| 44 |
+
response_text = await guardrail_service.validate_response(response_text)
|
| 45 |
+
except GuardrailViolation as e:
|
| 46 |
+
print(f"Safety Violation blocked: {e}")
|
| 47 |
+
response_text = "I apologize, but I cannot provide that response as it violates my safety guidelines regarding authority claims."
|
| 48 |
+
except GuardrailViolation:
|
| 49 |
+
response_text = "I apologize, but I cannot complete that response as it violates my safety guidelines."
|
| 50 |
+
|
| 51 |
+
return {
|
| 52 |
+
"role": "assistant",
|
| 53 |
+
"content": response_text,
|
| 54 |
+
"agent": swarm_response.agent.name if swarm_response.agent else "Unknown",
|
| 55 |
+
"trace": swarm_response.trace
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
orchestrator = OrchestratorService()
|
app/services/practices.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
|
| 4 |
+
class SpiritualPractice(BaseModel):
|
| 5 |
+
title: str
|
| 6 |
+
description: str
|
| 7 |
+
steps: List[str]
|
| 8 |
+
duration_minutes: int
|
| 9 |
+
scripture_ref: Optional[str] = None
|
| 10 |
+
|
| 11 |
+
class PracticeGenerator:
|
| 12 |
+
async def generate(self, intent_data: dict, profile_data: dict) -> SpiritualPractice:
|
| 13 |
+
# Generation logic here
|
| 14 |
+
return SpiritualPractice(
|
| 15 |
+
title="Lectio Divina",
|
| 16 |
+
description="A traditional practice of scriptural reading, meditation and prayer.",
|
| 17 |
+
steps=["Read", "Reflect", "Respond", "Rest"],
|
| 18 |
+
duration_minutes=15
|
| 19 |
+
)
|
app/services/prayer.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.services.llm import llm_service
|
| 2 |
+
|
| 3 |
+
class PrayerService:
|
| 4 |
+
PROMPT = """You are ORA, a prayer companion.
|
| 5 |
+
|
| 6 |
+
Prayer focus: {topic}
|
| 7 |
+
User emotion: {emotion}
|
| 8 |
+
Scripture (optional): {scripture}
|
| 9 |
+
|
| 10 |
+
Write a prayer that:
|
| 11 |
+
- Is gentle, humble, and invitational
|
| 12 |
+
- Avoids absolutes (e.g., "always", "never", "definitely")
|
| 13 |
+
- Does NOT make promises on God's behalf (e.g., "God will heal you")
|
| 14 |
+
- Does NOT use commands or imperatives (e.g., "Pray this", "Do this", "Trust me")
|
| 15 |
+
- Uses natural, conversational language, not sermon tone
|
| 16 |
+
- Expresses hope and longing without guaranteeing outcomes"""
|
| 17 |
+
|
| 18 |
+
async def compose(self, topic: str, emotion: str, scripture: str = "") -> str:
|
| 19 |
+
prompt = self.PROMPT.format(topic=topic, emotion=emotion, scripture=scripture)
|
| 20 |
+
return await llm_service.generate_response(message="Compose a prayer for me.", system_prompt=prompt)
|
| 21 |
+
|
| 22 |
+
prayer_service = PrayerService()
|
app/services/profile.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
from enum import Enum
|
| 4 |
+
import sqlite3
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
class TonePreference(str, Enum):
|
| 8 |
+
PASTORAL = "pastoral"
|
| 9 |
+
SCHOLARLY = "scholarly"
|
| 10 |
+
GENTLE = "gentle"
|
| 11 |
+
DIRECT = "direct"
|
| 12 |
+
|
| 13 |
+
class SpiritualGoal(str, Enum):
|
| 14 |
+
PRAYER = "prayer"
|
| 15 |
+
STUDY = "study"
|
| 16 |
+
HEALING = "healing"
|
| 17 |
+
LEADERSHIP = "leadership"
|
| 18 |
+
DISCERNMENT = "discernment"
|
| 19 |
+
|
| 20 |
+
class UserProfile(BaseModel):
|
| 21 |
+
user_id: str
|
| 22 |
+
denomination: Optional[str] = None
|
| 23 |
+
bible_translations: List[str] = Field(default=["NIV", "ESV"])
|
| 24 |
+
tone_preference: TonePreference = TonePreference.PASTORAL
|
| 25 |
+
spiritual_goals: List[SpiritualGoal] = []
|
| 26 |
+
|
| 27 |
+
class Config:
|
| 28 |
+
json_schema_extra = {
|
| 29 |
+
"example": {
|
| 30 |
+
"user_id": "usr_123",
|
| 31 |
+
"denomination": "non-denominational",
|
| 32 |
+
"bible_translations": ["NIV"],
|
| 33 |
+
"tone_preference": "gentle",
|
| 34 |
+
"spiritual_goals": ["prayer", "healing"]
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
class ProfileService:
|
| 39 |
+
def __init__(self, db_path: str = "memory.db"):
|
| 40 |
+
self.db_path = db_path
|
| 41 |
+
self._init_db()
|
| 42 |
+
|
| 43 |
+
def _init_db(self):
|
| 44 |
+
conn = sqlite3.connect(self.db_path)
|
| 45 |
+
cursor = conn.cursor()
|
| 46 |
+
cursor.execute('''
|
| 47 |
+
CREATE TABLE IF NOT EXISTS user_profiles (
|
| 48 |
+
user_id TEXT PRIMARY KEY,
|
| 49 |
+
denomination TEXT,
|
| 50 |
+
bible_translations TEXT,
|
| 51 |
+
tone_preference TEXT,
|
| 52 |
+
spiritual_goals TEXT
|
| 53 |
+
)
|
| 54 |
+
''')
|
| 55 |
+
conn.commit()
|
| 56 |
+
conn.close()
|
| 57 |
+
|
| 58 |
+
async def get_profile(self, user_id: str) -> UserProfile:
|
| 59 |
+
conn = sqlite3.connect(self.db_path)
|
| 60 |
+
conn.row_factory = sqlite3.Row
|
| 61 |
+
cursor = conn.cursor()
|
| 62 |
+
|
| 63 |
+
cursor.execute('SELECT * FROM user_profiles WHERE user_id = ?', (user_id,))
|
| 64 |
+
row = cursor.fetchone()
|
| 65 |
+
conn.close()
|
| 66 |
+
|
| 67 |
+
if row:
|
| 68 |
+
return UserProfile(
|
| 69 |
+
user_id=row['user_id'],
|
| 70 |
+
denomination=row['denomination'],
|
| 71 |
+
bible_translations=json.loads(row['bible_translations']) if row['bible_translations'] else ["NIV"],
|
| 72 |
+
tone_preference=row['tone_preference'] or TonePreference.PASTORAL,
|
| 73 |
+
spiritual_goals=json.loads(row['spiritual_goals']) if row['spiritual_goals'] else []
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
# Return default profile if none exists
|
| 77 |
+
return UserProfile(user_id=user_id)
|
| 78 |
+
|
| 79 |
+
async def update_profile(self, user_id: str, data: dict) -> UserProfile:
|
| 80 |
+
# Get current profile to merge (or default)
|
| 81 |
+
current = await self.get_profile(user_id)
|
| 82 |
+
|
| 83 |
+
# Merge data and validate to ensure Enums are correctly parsed
|
| 84 |
+
merged_data = current.model_dump()
|
| 85 |
+
merged_data.update(data)
|
| 86 |
+
updated_model = UserProfile(**merged_data)
|
| 87 |
+
|
| 88 |
+
conn = sqlite3.connect(self.db_path)
|
| 89 |
+
cursor = conn.cursor()
|
| 90 |
+
|
| 91 |
+
cursor.execute('''
|
| 92 |
+
INSERT OR REPLACE INTO user_profiles (user_id, denomination, bible_translations, tone_preference, spiritual_goals)
|
| 93 |
+
VALUES (?, ?, ?, ?, ?)
|
| 94 |
+
''', (
|
| 95 |
+
updated_model.user_id,
|
| 96 |
+
updated_model.denomination,
|
| 97 |
+
json.dumps(updated_model.bible_translations),
|
| 98 |
+
updated_model.tone_preference.value,
|
| 99 |
+
json.dumps([g.value for g in updated_model.spiritual_goals])
|
| 100 |
+
))
|
| 101 |
+
|
| 102 |
+
conn.commit()
|
| 103 |
+
conn.close()
|
| 104 |
+
|
| 105 |
+
return updated_model
|
| 106 |
+
|
| 107 |
+
profile_service = ProfileService()
|
app/services/repl.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import io
|
| 3 |
+
import contextlib
|
| 4 |
+
import multiprocessing
|
| 5 |
+
import traceback
|
| 6 |
+
from typing import Dict, Any, Optional
|
| 7 |
+
|
| 8 |
+
def _restricted_execute(code: str, queue: multiprocessing.Queue):
|
| 9 |
+
"""
|
| 10 |
+
Executes code in a completely isolated process environment.
|
| 11 |
+
"""
|
| 12 |
+
# Redirect stdout to capture it
|
| 13 |
+
stdout = io.StringIO()
|
| 14 |
+
|
| 15 |
+
# Define restricted globals
|
| 16 |
+
# We remove dangerous modules like 'os', 'sys', 'shutil', etc.
|
| 17 |
+
safe_globals = {
|
| 18 |
+
"__builtins__": __builtins__.copy(),
|
| 19 |
+
"math": __import__("math"),
|
| 20 |
+
"datetime": __import__("datetime"),
|
| 21 |
+
"json": __import__("json"),
|
| 22 |
+
}
|
| 23 |
+
# Remove dangerous builtins
|
| 24 |
+
dangerous_builtins = ["open", "exec", "eval", "getattr", "setattr", "delattr", "help", "input", "compile"]
|
| 25 |
+
for b in dangerous_builtins:
|
| 26 |
+
if b in safe_globals["__builtins__"]:
|
| 27 |
+
del safe_globals["__builtins__"][b]
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
with contextlib.redirect_stdout(stdout):
|
| 31 |
+
exec(code, safe_globals)
|
| 32 |
+
queue.put({"success": True, "output": stdout.getvalue(), "error": None})
|
| 33 |
+
except Exception:
|
| 34 |
+
queue.put({"success": False, "output": stdout.getvalue(), "error": traceback.format_exc()})
|
| 35 |
+
|
| 36 |
+
class REPLService:
|
| 37 |
+
def execute(self, code: str, timeout: int = 2) -> Dict[str, Any]:
|
| 38 |
+
"""
|
| 39 |
+
Executes Python code in a restricted global environment.
|
| 40 |
+
"""
|
| 41 |
+
stdout = io.StringIO()
|
| 42 |
+
|
| 43 |
+
# Define restricted globals
|
| 44 |
+
safe_globals = {
|
| 45 |
+
"__builtins__": {
|
| 46 |
+
"print": print,
|
| 47 |
+
"range": range,
|
| 48 |
+
"len": len,
|
| 49 |
+
"list": list,
|
| 50 |
+
"dict": dict,
|
| 51 |
+
"str": str,
|
| 52 |
+
"int": int,
|
| 53 |
+
"float": float,
|
| 54 |
+
"bool": bool,
|
| 55 |
+
"abs": abs,
|
| 56 |
+
"sum": sum,
|
| 57 |
+
"min": min,
|
| 58 |
+
"max": max,
|
| 59 |
+
"reversed": reversed,
|
| 60 |
+
"sorted": sorted,
|
| 61 |
+
"set": set,
|
| 62 |
+
"enumerate": enumerate,
|
| 63 |
+
"zip": zip,
|
| 64 |
+
},
|
| 65 |
+
"math": __import__("math"),
|
| 66 |
+
"datetime": __import__("datetime"),
|
| 67 |
+
"json": __import__("json"),
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
try:
|
| 71 |
+
with contextlib.redirect_stdout(stdout):
|
| 72 |
+
# Using a wrapper to avoid affecting the main thread too much if it hangs
|
| 73 |
+
# Note: This doesn't actually provide timeout protection in same-thread
|
| 74 |
+
# but works around the multiprocess hang in local dev
|
| 75 |
+
exec(code, safe_globals)
|
| 76 |
+
|
| 77 |
+
return {
|
| 78 |
+
"success": True,
|
| 79 |
+
"output": stdout.getvalue(),
|
| 80 |
+
"error": None
|
| 81 |
+
}
|
| 82 |
+
except Exception:
|
| 83 |
+
return {
|
| 84 |
+
"success": False,
|
| 85 |
+
"output": stdout.getvalue(),
|
| 86 |
+
"error": traceback.format_exc()
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
repl_service = REPLService()
|
app/services/rlm.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.services.repl import repl_service
|
| 2 |
+
from app.services.llm import llm_service
|
| 3 |
+
from typing import Dict, Any
|
| 4 |
+
|
| 5 |
+
class RLMService:
|
| 6 |
+
"""
|
| 7 |
+
Reason Synergizing Service.
|
| 8 |
+
Uses the REPL to solve complex logical problems through code generation and execution.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
async def reason(self, problem: str) -> str:
|
| 12 |
+
"""
|
| 13 |
+
Takes a logical problem, generates Python code to solve it, and returns the result.
|
| 14 |
+
"""
|
| 15 |
+
system_prompt = """
|
| 16 |
+
You are the ORA Reasoning Engine.
|
| 17 |
+
Your task is to write a Python script to solve the user's logical problem or calculation.
|
| 18 |
+
- Only output the Python code, nothing else.
|
| 19 |
+
- Use print() to output final results.
|
| 20 |
+
- You have access to: math, datetime, json.
|
| 21 |
+
- Do not use dangerous modules (os, subprocess, etc).
|
| 22 |
+
- Keep the script efficient.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
# 1. Generate the code
|
| 26 |
+
prompt = f"Problem: {problem}\n\nWrite a Python script to solve this. Only output code."
|
| 27 |
+
llm_res = await llm_service.generate_response(prompt, system_prompt=system_prompt)
|
| 28 |
+
code = llm_res.get("content", "").strip()
|
| 29 |
+
|
| 30 |
+
# Clean up markdown if LLM adds it
|
| 31 |
+
if code.startswith("```python"):
|
| 32 |
+
code = code.split("```python")[1].split("```")[0].strip()
|
| 33 |
+
elif code.startswith("```"):
|
| 34 |
+
code = code.split("```")[1].split("```")[0].strip()
|
| 35 |
+
|
| 36 |
+
if not code:
|
| 37 |
+
return "Error: Could not generate reasoning code."
|
| 38 |
+
|
| 39 |
+
# 2. Execute in REPL
|
| 40 |
+
print(f"RLM: Executing reasoning code...")
|
| 41 |
+
result = repl_service.execute(code)
|
| 42 |
+
|
| 43 |
+
if not result["success"]:
|
| 44 |
+
return f"Reasoning Fallacy (Code Error): {result['error']}"
|
| 45 |
+
|
| 46 |
+
return result["output"]
|
| 47 |
+
|
| 48 |
+
rlm_service = RLMService()
|
app/services/simplify.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.services.llm import llm_service
|
| 2 |
+
|
| 3 |
+
class SimplifyService:
|
| 4 |
+
PROMPT = """Rewrite the following spiritual explanation
|
| 5 |
+
in clear, everyday language.
|
| 6 |
+
|
| 7 |
+
Text:
|
| 8 |
+
{text}
|
| 9 |
+
|
| 10 |
+
Rules:
|
| 11 |
+
- No jargon
|
| 12 |
+
- No preaching
|
| 13 |
+
- Keep meaning intact"""
|
| 14 |
+
|
| 15 |
+
async def simplify(self, text: str) -> str:
|
| 16 |
+
prompt = self.PROMPT.format(text=text)
|
| 17 |
+
return await llm_service.generate_response(message="Simplify this.", system_prompt=prompt)
|
| 18 |
+
|
| 19 |
+
simplify_service = SimplifyService()
|
app/services/stillness.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.services.llm import llm_service
|
| 2 |
+
|
| 3 |
+
class StillnessService:
|
| 4 |
+
PROMPT = """You are guiding the user into silence and stillness.
|
| 5 |
+
|
| 6 |
+
Create:
|
| 7 |
+
- A short breathing or grounding exercise
|
| 8 |
+
- One reflective sentence
|
| 9 |
+
- An invitation to pause without pressure
|
| 10 |
+
|
| 11 |
+
Do not quote scripture unless requested.
|
| 12 |
+
Keep it under 150 words."""
|
| 13 |
+
|
| 14 |
+
async def guide(self) -> str:
|
| 15 |
+
return await llm_service.generate_response(message="Help me find stillness.", system_prompt=self.PROMPT)
|
| 16 |
+
|
| 17 |
+
stillness_service = StillnessService()
|
app/services/trace.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from typing import Dict, Any
|
| 5 |
+
|
| 6 |
+
class TraceService:
|
| 7 |
+
TRACE_DIR = "important/reasoning_traces"
|
| 8 |
+
|
| 9 |
+
def __init__(self):
|
| 10 |
+
if not os.path.exists(self.TRACE_DIR):
|
| 11 |
+
os.makedirs(self.TRACE_DIR, exist_ok=True)
|
| 12 |
+
|
| 13 |
+
def save_trace(self, trace_data: Dict[str, Any]):
|
| 14 |
+
"""
|
| 15 |
+
Appends a reasoning trace to a daily .jsonl file.
|
| 16 |
+
"""
|
| 17 |
+
today = datetime.now().strftime("%Y-%m-%d")
|
| 18 |
+
file_path = os.path.join(self.TRACE_DIR, f"ora_traces_{today}.jsonl")
|
| 19 |
+
|
| 20 |
+
# Add timestamp if missing
|
| 21 |
+
if "timestamp" not in trace_data:
|
| 22 |
+
trace_data["timestamp"] = datetime.now().isoformat()
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
with open(file_path, "a", encoding="utf-8") as f:
|
| 26 |
+
f.write(json.dumps(trace_data) + "\n")
|
| 27 |
+
# print(f"TraceService: Trace saved to {file_path}")
|
| 28 |
+
except Exception as e:
|
| 29 |
+
print(f"TraceService Error: Failed to save trace: {str(e)}")
|
| 30 |
+
|
| 31 |
+
trace_service = TraceService()
|
app/services/translation.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.services.llm import llm_service
|
| 2 |
+
from typing import List
|
| 3 |
+
|
| 4 |
+
class TranslationService:
|
| 5 |
+
PROMPT = """Compare the following Bible translations:
|
| 6 |
+
|
| 7 |
+
Verse reference: {reference}
|
| 8 |
+
Translations:
|
| 9 |
+
{translations}
|
| 10 |
+
|
| 11 |
+
Explain:
|
| 12 |
+
- Key wording differences
|
| 13 |
+
- What meaning is shared
|
| 14 |
+
- What remains ambiguous
|
| 15 |
+
|
| 16 |
+
Do not claim one translation is superior."""
|
| 17 |
+
|
| 18 |
+
async def compare(self, reference: str, translations: List[str]) -> str:
|
| 19 |
+
# In a real app, we would fetch the texts first, here we mock passing them
|
| 20 |
+
translations_text = "\n".join(translations) # Placeholder
|
| 21 |
+
prompt = self.PROMPT.format(reference=reference, translations=translations_text)
|
| 22 |
+
return await llm_service.generate_response(message="Compare these translations.", system_prompt=prompt)
|
| 23 |
+
|
| 24 |
+
translation_service = TranslationService()
|
config.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{}
|
frontend/.claude/settings.local.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"permissions": {
|
| 3 |
+
"allow": [
|
| 4 |
+
"Bash(dir:*)",
|
| 5 |
+
"Bash(npm run build:*)",
|
| 6 |
+
"Bash(npm run dev:*)",
|
| 7 |
+
"Bash(taskkill:*)",
|
| 8 |
+
"Bash(timeout /t 2)",
|
| 9 |
+
"Bash(cat:*)",
|
| 10 |
+
"Bash(if [ -d \".next\" ])",
|
| 11 |
+
"Bash(then rm -rf .next)",
|
| 12 |
+
"Bash(fi)",
|
| 13 |
+
"Bash(git init:*)",
|
| 14 |
+
"Bash(git add:*)"
|
| 15 |
+
]
|
| 16 |
+
}
|
| 17 |
+
}
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dependencies
|
| 2 |
+
/node_modules
|
| 3 |
+
/.pnp
|
| 4 |
+
.pnp.js
|
| 5 |
+
|
| 6 |
+
# Testing
|
| 7 |
+
/coverage
|
| 8 |
+
|
| 9 |
+
# Next.js
|
| 10 |
+
/.next/
|
| 11 |
+
/out/
|
| 12 |
+
|
| 13 |
+
# Production
|
| 14 |
+
/build
|
| 15 |
+
|
| 16 |
+
# Misc
|
| 17 |
+
.DS_Store
|
| 18 |
+
*.pem
|
| 19 |
+
|
| 20 |
+
# Debug
|
| 21 |
+
npm-debug.log*
|
| 22 |
+
yarn-debug.log*
|
| 23 |
+
yarn-error.log*
|
| 24 |
+
|
| 25 |
+
# Local env files
|
| 26 |
+
.env*.local
|
| 27 |
+
|
| 28 |
+
# Vercel
|
| 29 |
+
.vercel
|
| 30 |
+
|
| 31 |
+
# TypeScript
|
| 32 |
+
*.tsbuildinfo
|
| 33 |
+
next-env.d.ts
|
frontend/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SoapBox Candles - Web App
|
| 2 |
+
|
| 3 |
+
A Next.js web application for SoapBox Super App's Candles feature - spiritual engagement rewards and AI feature credits.
|
| 4 |
+
|
| 5 |
+
## Getting Started
|
| 6 |
+
|
| 7 |
+
### Prerequisites
|
| 8 |
+
- Node.js 18+
|
| 9 |
+
- npm or yarn
|
| 10 |
+
|
| 11 |
+
### Installation
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
cd soapbox-candles
|
| 15 |
+
npm install
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
### Development
|
| 19 |
+
|
| 20 |
+
```bash
|
| 21 |
+
npm run dev
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
Open [http://localhost:3000](http://localhost:3000) in your browser.
|
| 25 |
+
|
| 26 |
+
### Build
|
| 27 |
+
|
| 28 |
+
```bash
|
| 29 |
+
npm run build
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
### Production
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
npm start
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## Deploy to Vercel
|
| 39 |
+
|
| 40 |
+
### Option 1: Vercel CLI
|
| 41 |
+
|
| 42 |
+
```bash
|
| 43 |
+
npm i -g vercel
|
| 44 |
+
vercel
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### Option 2: GitHub Integration
|
| 48 |
+
|
| 49 |
+
1. Push this project to a GitHub repository
|
| 50 |
+
2. Go to [vercel.com](https://vercel.com)
|
| 51 |
+
3. Import your GitHub repository
|
| 52 |
+
4. Vercel will auto-detect Next.js and deploy
|
| 53 |
+
|
| 54 |
+
## Project Structure
|
| 55 |
+
|
| 56 |
+
```
|
| 57 |
+
soapbox-candles/
|
| 58 |
+
├── app/
|
| 59 |
+
│ ├── globals.css # Global styles & animations
|
| 60 |
+
│ ├── layout.tsx # Root layout with Navbar/Footer
|
| 61 |
+
│ ├── page.tsx # Home page (Candles landing)
|
| 62 |
+
│ ├── features/ # Features page
|
| 63 |
+
│ ├── pricing/ # Pricing page
|
| 64 |
+
│ ├── about/ # About page
|
| 65 |
+
│ ├── contact/ # Contact page
|
| 66 |
+
│ ├── signin/ # Sign in page
|
| 67 |
+
│ └── get-started/ # Sign up page
|
| 68 |
+
├── components/
|
| 69 |
+
│ ├── Navbar.tsx # Navigation component
|
| 70 |
+
│ ├── Footer.tsx # Footer component
|
| 71 |
+
│ ├── SpotlightCard.tsx # Interactive card component
|
| 72 |
+
│ └── ShinyButton.tsx # Animated CTA button
|
| 73 |
+
├── public/ # Static assets
|
| 74 |
+
├── package.json
|
| 75 |
+
├── tailwind.config.js
|
| 76 |
+
├── tsconfig.json
|
| 77 |
+
└── next.config.js
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
## Features
|
| 81 |
+
|
| 82 |
+
- **Responsive Design** - Works on all devices
|
| 83 |
+
- **Animations** - Sonar, beam, float, flicker effects
|
| 84 |
+
- **Interactive Cards** - Spotlight effect on hover
|
| 85 |
+
- **Shiny CTA Buttons** - Animated gradient borders
|
| 86 |
+
- **Dark Theme** - Warm amber/gold color scheme
|
| 87 |
+
- **TypeScript** - Full type safety
|
| 88 |
+
- **Tailwind CSS** - Utility-first styling
|
| 89 |
+
|
| 90 |
+
## Pages
|
| 91 |
+
|
| 92 |
+
- `/` - Candles landing page
|
| 93 |
+
- `/features` - Platform features
|
| 94 |
+
- `/pricing` - Pricing plans
|
| 95 |
+
- `/about` - About SoapBox
|
| 96 |
+
- `/contact` - Contact form
|
| 97 |
+
- `/signin` - Sign in
|
| 98 |
+
- `/get-started` - Sign up
|
frontend/app/about/page.tsx
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { Sparkles, Brain, Heart, BookOpen, Shield, Users, Zap, Target } from 'lucide-react';
|
| 4 |
+
import SpotlightCard from '@/components/SpotlightCard';
|
| 5 |
+
import Link from 'next/link';
|
| 6 |
+
import ShinyButton from '@/components/ShinyButton';
|
| 7 |
+
|
| 8 |
+
const values = [
|
| 9 |
+
{
|
| 10 |
+
icon: Shield,
|
| 11 |
+
title: 'Faith-First Design',
|
| 12 |
+
description: 'Every feature is built with theological accuracy and spiritual sensitivity at its core.',
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
icon: Heart,
|
| 16 |
+
title: 'Compassionate AI',
|
| 17 |
+
description: 'Our agents are trained to provide empathetic, pastoral care in every interaction.',
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
icon: Brain,
|
| 21 |
+
title: 'Intelligent Guidance',
|
| 22 |
+
description: 'Multi-agent architecture ensures you receive specialized expertise for every question.',
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
icon: Users,
|
| 26 |
+
title: 'Privacy Respected',
|
| 27 |
+
description: 'Your spiritual journey is sacred. We never sell data or compromise your privacy.',
|
| 28 |
+
},
|
| 29 |
+
];
|
| 30 |
+
|
| 31 |
+
const agents = [
|
| 32 |
+
{
|
| 33 |
+
name: 'Gatekeeper',
|
| 34 |
+
icon: Shield,
|
| 35 |
+
color: 'purple',
|
| 36 |
+
description: 'The intelligent router that understands your intent and connects you with the right specialist.',
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
name: 'Theologian',
|
| 40 |
+
icon: BookOpen,
|
| 41 |
+
color: 'blue',
|
| 42 |
+
description: 'Deep Scripture analysis with cross-references, historical context, and doctrinal insights.',
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
name: 'Healer',
|
| 46 |
+
icon: Heart,
|
| 47 |
+
color: 'rose',
|
| 48 |
+
description: 'Compassionate pastoral care with prayer guidance and emotional support.',
|
| 49 |
+
},
|
| 50 |
+
];
|
| 51 |
+
|
| 52 |
+
const colorClasses: Record<string, { bg: string; border: string; text: string }> = {
|
| 53 |
+
purple: { bg: 'bg-purple-500/10', border: 'border-purple-500/30', text: 'text-purple-400' },
|
| 54 |
+
blue: { bg: 'bg-blue-500/10', border: 'border-blue-500/30', text: 'text-blue-400' },
|
| 55 |
+
rose: { bg: 'bg-rose-500/10', border: 'border-rose-500/30', text: 'text-rose-400' },
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
export default function AboutPage() {
|
| 59 |
+
return (
|
| 60 |
+
<>
|
| 61 |
+
{/* Hero */}
|
| 62 |
+
<section className="relative pt-32 pb-20 overflow-hidden">
|
| 63 |
+
<div className="absolute inset-0 ora-grid-bg pointer-events-none z-0" />
|
| 64 |
+
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[600px] bg-purple-600/20 rounded-full blur-[150px] pointer-events-none -z-10" />
|
| 65 |
+
|
| 66 |
+
<div className="max-w-4xl mx-auto px-6 text-center">
|
| 67 |
+
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-purple-500/30 bg-purple-500/10 mb-8">
|
| 68 |
+
<Sparkles className="w-4 h-4 text-purple-400" />
|
| 69 |
+
<span className="text-xs font-mono text-purple-300 uppercase tracking-wider">About ORA</span>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<h1 className="text-4xl md:text-5xl font-bold tracking-tight mb-6 text-white">
|
| 73 |
+
AI Spiritual Guidance,{' '}
|
| 74 |
+
<span className="text-purple-400">Reimagined</span>
|
| 75 |
+
</h1>
|
| 76 |
+
|
| 77 |
+
<p className="text-lg text-neutral-400 max-w-2xl mx-auto">
|
| 78 |
+
ORA is a sovereign AI spiritual companion built with a multi-agent architecture
|
| 79 |
+
designed to provide personalized, theologically-grounded guidance for your faith journey.
|
| 80 |
+
</p>
|
| 81 |
+
</div>
|
| 82 |
+
</section>
|
| 83 |
+
|
| 84 |
+
{/* Mission */}
|
| 85 |
+
<section className="py-20 border-t border-white/5">
|
| 86 |
+
<div className="max-w-6xl mx-auto px-6">
|
| 87 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
| 88 |
+
<div>
|
| 89 |
+
<h2 className="text-3xl font-bold text-white mb-6">Our Mission</h2>
|
| 90 |
+
<p className="text-neutral-400 leading-relaxed mb-4">
|
| 91 |
+
We believe technology can serve faith. ORA was created to bridge the gap between
|
| 92 |
+
ancient wisdom and modern AI, providing believers with an intelligent companion
|
| 93 |
+
that understands Scripture, respects tradition, and meets you where you are.
|
| 94 |
+
</p>
|
| 95 |
+
<p className="text-neutral-400 leading-relaxed">
|
| 96 |
+
Our name embodies our method: <strong className="text-purple-400">O</strong>bserve,
|
| 97 |
+
<strong className="text-purple-400"> R</strong>eflect, <strong className="text-purple-400">A</strong>ct.
|
| 98 |
+
This framework guides every interaction, helping you engage more deeply with your faith.
|
| 99 |
+
</p>
|
| 100 |
+
</div>
|
| 101 |
+
<div className="grid grid-cols-2 gap-4">
|
| 102 |
+
{values.map((value) => (
|
| 103 |
+
<div
|
| 104 |
+
key={value.title}
|
| 105 |
+
className="p-5 rounded-xl bg-white/[0.02] border border-white/10 hover:border-purple-500/30 transition-all"
|
| 106 |
+
>
|
| 107 |
+
<value.icon className="w-8 h-8 text-purple-400 mb-3" />
|
| 108 |
+
<h3 className="text-white font-medium mb-1">{value.title}</h3>
|
| 109 |
+
<p className="text-neutral-500 text-sm">{value.description}</p>
|
| 110 |
+
</div>
|
| 111 |
+
))}
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
</section>
|
| 116 |
+
|
| 117 |
+
{/* Agent Architecture */}
|
| 118 |
+
<section className="py-20 border-t border-white/5 bg-[#030303]">
|
| 119 |
+
<div className="max-w-6xl mx-auto px-6">
|
| 120 |
+
<div className="text-center mb-12">
|
| 121 |
+
<h2 className="text-3xl font-bold text-white mb-4">Multi-Agent Architecture</h2>
|
| 122 |
+
<p className="text-neutral-400 max-w-2xl mx-auto">
|
| 123 |
+
ORA uses specialized AI agents that work together, each bringing unique expertise
|
| 124 |
+
to serve your spiritual needs.
|
| 125 |
+
</p>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
| 129 |
+
{agents.map((agent) => {
|
| 130 |
+
const colors = colorClasses[agent.color];
|
| 131 |
+
return (
|
| 132 |
+
<SpotlightCard
|
| 133 |
+
key={agent.name}
|
| 134 |
+
className={`p-6 rounded-xl border ${colors.border} bg-white/[0.02]`}
|
| 135 |
+
spotlightColor="purple"
|
| 136 |
+
>
|
| 137 |
+
<div className={`w-14 h-14 rounded-xl ${colors.bg} flex items-center justify-center border ${colors.border} mb-4`}>
|
| 138 |
+
<agent.icon className={`w-7 h-7 ${colors.text}`} />
|
| 139 |
+
</div>
|
| 140 |
+
<h3 className="text-xl font-semibold text-white mb-2">{agent.name}</h3>
|
| 141 |
+
<p className="text-neutral-400 text-sm">{agent.description}</p>
|
| 142 |
+
</SpotlightCard>
|
| 143 |
+
);
|
| 144 |
+
})}
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
</section>
|
| 148 |
+
|
| 149 |
+
{/* Technology */}
|
| 150 |
+
<section className="py-20 border-t border-white/5">
|
| 151 |
+
<div className="max-w-4xl mx-auto px-6 text-center">
|
| 152 |
+
<h2 className="text-3xl font-bold text-white mb-6">Built with Purpose</h2>
|
| 153 |
+
<p className="text-neutral-400 mb-8 max-w-2xl mx-auto">
|
| 154 |
+
ORA combines cutting-edge AI with deep respect for theological tradition.
|
| 155 |
+
Our system includes episodic memory, reasoning traces, and safety guardrails
|
| 156 |
+
to ensure every interaction is helpful, accurate, and spiritually sensitive.
|
| 157 |
+
</p>
|
| 158 |
+
|
| 159 |
+
<div className="flex flex-wrap justify-center gap-3 mb-12">
|
| 160 |
+
{['Multi-Agent Swarm', 'Episodic Memory', 'LanceDB Vectors', 'Safety Guardrails', 'Reasoning Traces', 'Bible RAG'].map((tech) => (
|
| 161 |
+
<span
|
| 162 |
+
key={tech}
|
| 163 |
+
className="px-4 py-2 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-300 text-sm"
|
| 164 |
+
>
|
| 165 |
+
{tech}
|
| 166 |
+
</span>
|
| 167 |
+
))}
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
<Link href="/dashboard">
|
| 171 |
+
<ShinyButton>
|
| 172 |
+
<span className="flex items-center gap-2">
|
| 173 |
+
<Sparkles className="w-5 h-5" />
|
| 174 |
+
Experience ORA
|
| 175 |
+
</span>
|
| 176 |
+
</ShinyButton>
|
| 177 |
+
</Link>
|
| 178 |
+
</div>
|
| 179 |
+
</section>
|
| 180 |
+
</>
|
| 181 |
+
);
|
| 182 |
+
}
|
frontend/app/candle/page.tsx
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { Flame, Sun, Share2, TrendingUp, Gift, Calendar, Check, Users, Church, Star, Zap, ArrowRight, Sparkles, Heart, BookOpen, Video, MessageSquare, Trophy, Target, UserPlus, ThumbsUp } from 'lucide-react';
|
| 4 |
+
import SpotlightCard from '@/components/SpotlightCard';
|
| 5 |
+
import ShinyButton from '@/components/ShinyButton';
|
| 6 |
+
import Link from 'next/link';
|
| 7 |
+
|
| 8 |
+
export default function CandlePage() {
|
| 9 |
+
return (
|
| 10 |
+
<>
|
| 11 |
+
{/* Hero Section - Completely Redesigned */}
|
| 12 |
+
<section className="relative pt-36 pb-24 md:pt-44 md:pb-32 overflow-hidden min-h-[90vh] flex items-center">
|
| 13 |
+
<div className="absolute inset-0 bg-grid-pattern opacity-50 z-0 pointer-events-none" style={{ maskImage: 'radial-gradient(circle at center, black 30%, transparent 80%)', WebkitMaskImage: 'radial-gradient(circle at center, black 30%, transparent 80%)' }} />
|
| 14 |
+
|
| 15 |
+
{/* Animated Glow Backgrounds */}
|
| 16 |
+
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[1000px] h-[1000px] bg-amber-600/20 rounded-full blur-[150px] pointer-events-none -z-10 animate-pulse" style={{ animationDuration: '4s' }} />
|
| 17 |
+
<div className="absolute bottom-0 right-0 w-[600px] h-[600px] bg-orange-900/15 rounded-full blur-[100px] pointer-events-none -z-10" />
|
| 18 |
+
<div className="absolute top-1/2 left-0 w-[400px] h-[400px] bg-amber-500/10 rounded-full blur-[80px] pointer-events-none -z-10 animate-pulse" style={{ animationDuration: '6s', animationDelay: '2s' }} />
|
| 19 |
+
|
| 20 |
+
<div className="max-w-7xl mx-auto px-6 w-full">
|
| 21 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
|
| 22 |
+
{/* Left: Content */}
|
| 23 |
+
<div className="max-w-xl z-10">
|
| 24 |
+
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-amber-500/20 bg-amber-500/5 mb-8 backdrop-blur-sm animate-fade-slide-in">
|
| 25 |
+
<span className="relative flex h-2 w-2">
|
| 26 |
+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75" />
|
| 27 |
+
<span className="relative inline-flex rounded-full h-2 w-2 bg-amber-500" />
|
| 28 |
+
</span>
|
| 29 |
+
<span className="text-xs uppercase tracking-widest font-medium text-amber-200/80">
|
| 30 |
+
Spiritual Rewards System
|
| 31 |
+
</span>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<h1 className="text-5xl sm:text-6xl lg:text-7xl font-semibold tracking-tight leading-[1.05] mb-6 animate-fade-slide-in stagger-1">
|
| 35 |
+
<span className="text-white">Light Your</span>
|
| 36 |
+
<br />
|
| 37 |
+
<span className="text-transparent bg-clip-text bg-gradient-to-r from-amber-400 via-orange-400 to-amber-500">
|
| 38 |
+
Faith Journey
|
| 39 |
+
</span>
|
| 40 |
+
</h1>
|
| 41 |
+
|
| 42 |
+
<p className="text-xl text-neutral-300 mb-8 leading-relaxed animate-fade-slide-in stagger-2">
|
| 43 |
+
Earn <span className="text-amber-400 font-medium">Candles</span> through spiritual engagement. Spend them on AI-powered tools. Watch your faith community grow.
|
| 44 |
+
</p>
|
| 45 |
+
|
| 46 |
+
{/* Stats Row */}
|
| 47 |
+
<div className="flex items-center gap-6 mb-10 animate-fade-slide-in stagger-3">
|
| 48 |
+
<div className="text-center">
|
| 49 |
+
<div className="text-3xl font-bold text-amber-400">250</div>
|
| 50 |
+
<div className="text-xs text-neutral-500 uppercase tracking-wider">Free Start</div>
|
| 51 |
+
</div>
|
| 52 |
+
<div className="w-px h-12 bg-white/10" />
|
| 53 |
+
<div className="text-center">
|
| 54 |
+
<div className="text-3xl font-bold text-amber-400">+250</div>
|
| 55 |
+
<div className="text-xs text-neutral-500 uppercase tracking-wider">Monthly</div>
|
| 56 |
+
</div>
|
| 57 |
+
<div className="w-px h-12 bg-white/10" />
|
| 58 |
+
<div className="text-center">
|
| 59 |
+
<div className="text-3xl font-bold text-emerald-400">∞</div>
|
| 60 |
+
<div className="text-xs text-neutral-500 uppercase tracking-wider">Earn More</div>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<div className="flex flex-col sm:flex-row gap-4 animate-fade-slide-in stagger-4">
|
| 65 |
+
<Link href="/get-started">
|
| 66 |
+
<ShinyButton>
|
| 67 |
+
<span className="flex items-center gap-2">
|
| 68 |
+
<Flame className="w-[18px] h-[18px] candle-glow" />
|
| 69 |
+
Get 250 Free Candles
|
| 70 |
+
</span>
|
| 71 |
+
</ShinyButton>
|
| 72 |
+
</Link>
|
| 73 |
+
<button className="px-6 py-3.5 text-sm font-medium text-neutral-300 border border-white/10 rounded-full hover:bg-white/5 hover:text-white transition-all flex items-center gap-2">
|
| 74 |
+
<span>See How It Works</span>
|
| 75 |
+
<ArrowRight className="w-4 h-4" />
|
| 76 |
+
</button>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
{/* Right: Candle Visual System */}
|
| 81 |
+
<div className="relative h-[500px] lg:h-[600px] w-full flex items-center justify-center animate-fade-slide-in stagger-2">
|
| 82 |
+
{/* Central Candle Orb */}
|
| 83 |
+
<div className="relative z-20">
|
| 84 |
+
{/* Outer Glow Ring */}
|
| 85 |
+
<div className="absolute inset-0 w-64 h-64 -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2">
|
| 86 |
+
<div className="absolute inset-0 rounded-full border border-amber-500/20 animate-ping" style={{ animationDuration: '3s' }} />
|
| 87 |
+
<div className="absolute inset-4 rounded-full border border-amber-500/30 animate-ping" style={{ animationDuration: '3s', animationDelay: '1s' }} />
|
| 88 |
+
<div className="absolute inset-8 rounded-full border border-amber-500/40 animate-ping" style={{ animationDuration: '3s', animationDelay: '2s' }} />
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
+
{/* Main Candle Circle */}
|
| 92 |
+
<div className="w-48 h-48 rounded-full bg-gradient-to-br from-amber-500/20 via-orange-500/10 to-transparent border border-amber-500/30 flex items-center justify-center shadow-[0_0_100px_-20px_rgba(251,191,36,0.5)] animate-breathe">
|
| 93 |
+
<div className="w-36 h-36 rounded-full bg-gradient-to-br from-amber-500/30 to-orange-600/20 border border-amber-400/40 flex items-center justify-center">
|
| 94 |
+
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-amber-400/50 to-orange-500/30 border border-amber-300/50 flex items-center justify-center shadow-[inset_0_0_30px_rgba(251,191,36,0.3)]">
|
| 95 |
+
<Flame className="w-12 h-12 text-amber-300 drop-shadow-[0_0_20px_rgba(251,191,36,0.8)] animate-flicker" />
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
{/* Orbiting Elements */}
|
| 101 |
+
<div className="absolute inset-0 w-72 h-72 -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2 animate-spin-slow">
|
| 102 |
+
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
| 103 |
+
<div className="w-10 h-10 rounded-full bg-amber-500/20 border border-amber-500/30 flex items-center justify-center">
|
| 104 |
+
<Sun className="w-5 h-5 text-amber-400" />
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2">
|
| 108 |
+
<div className="w-10 h-10 rounded-full bg-rose-500/20 border border-rose-500/30 flex items-center justify-center">
|
| 109 |
+
<Heart className="w-5 h-5 text-rose-400" />
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
<div className="absolute left-0 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
| 113 |
+
<div className="w-10 h-10 rounded-full bg-blue-500/20 border border-blue-500/30 flex items-center justify-center">
|
| 114 |
+
<Share2 className="w-5 h-5 text-blue-400" />
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
<div className="absolute right-0 top-1/2 translate-x-1/2 -translate-y-1/2">
|
| 118 |
+
<div className="w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/30 flex items-center justify-center">
|
| 119 |
+
<TrendingUp className="w-5 h-5 text-emerald-400" />
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
|
| 124 |
+
{/* Inner Orbit */}
|
| 125 |
+
<div className="absolute inset-0 w-56 h-56 -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2 animate-spin-slow" style={{ animationDirection: 'reverse', animationDuration: '20s' }}>
|
| 126 |
+
<div className="absolute top-0 right-0 translate-x-1/4 -translate-y-1/4">
|
| 127 |
+
<div className="w-8 h-8 rounded-full bg-purple-500/20 border border-purple-500/30 flex items-center justify-center">
|
| 128 |
+
<Sparkles className="w-4 h-4 text-purple-400" />
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
<div className="absolute bottom-0 left-0 -translate-x-1/4 translate-y-1/4">
|
| 132 |
+
<div className="w-8 h-8 rounded-full bg-cyan-500/20 border border-cyan-500/30 flex items-center justify-center">
|
| 133 |
+
<Zap className="w-4 h-4 text-cyan-400" />
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
|
| 139 |
+
{/* Floating Cards */}
|
| 140 |
+
<div className="absolute top-8 right-0 p-4 rounded-2xl bg-white/5 border border-white/10 backdrop-blur-xl flex items-center gap-3 animate-float" style={{ animationDuration: '6s' }}>
|
| 141 |
+
<div className="w-10 h-10 rounded-xl bg-amber-500/20 flex items-center justify-center">
|
| 142 |
+
<Gift className="w-5 h-5 text-amber-400" />
|
| 143 |
+
</div>
|
| 144 |
+
<div>
|
| 145 |
+
<div className="text-[10px] text-neutral-500 uppercase tracking-wider">Welcome Bonus</div>
|
| 146 |
+
<div className="text-lg font-bold text-white">+250 <span className="text-amber-400 text-sm">Candles</span></div>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
|
| 150 |
+
<div className="absolute bottom-16 left-0 p-4 rounded-2xl bg-white/5 border border-white/10 backdrop-blur-xl flex items-center gap-3 animate-float" style={{ animationDuration: '7s', animationDelay: '1s' }}>
|
| 151 |
+
<div className="w-10 h-10 rounded-xl bg-emerald-500/20 flex items-center justify-center">
|
| 152 |
+
<Calendar className="w-5 h-5 text-emerald-400" />
|
| 153 |
+
</div>
|
| 154 |
+
<div>
|
| 155 |
+
<div className="text-[10px] text-neutral-500 uppercase tracking-wider">Monthly Refresh</div>
|
| 156 |
+
<div className="text-lg font-bold text-white">+250 <span className="text-emerald-400 text-sm">Candles</span></div>
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<div className="absolute top-1/2 right-8 -translate-y-1/2 p-3 rounded-xl bg-purple-500/10 border border-purple-500/20 backdrop-blur-xl animate-float" style={{ animationDuration: '8s', animationDelay: '2s' }}>
|
| 161 |
+
<div className="flex items-center gap-2">
|
| 162 |
+
<Sparkles className="w-4 h-4 text-purple-400" />
|
| 163 |
+
<span className="text-xs text-purple-300 font-medium">ORA™ Powered</span>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
</section>
|
| 170 |
+
|
| 171 |
+
{/* How Candles Work - Interactive Visual */}
|
| 172 |
+
<section className="py-24 relative border-t border-white/5">
|
| 173 |
+
<div className="max-w-7xl mx-auto px-6">
|
| 174 |
+
<div className="text-center mb-16">
|
| 175 |
+
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full border border-amber-500/10 bg-amber-500/5 mb-6">
|
| 176 |
+
<Flame className="w-3.5 h-3.5 text-amber-400" />
|
| 177 |
+
<span className="text-[10px] uppercase tracking-widest font-medium text-amber-200/80">The Candle Economy</span>
|
| 178 |
+
</div>
|
| 179 |
+
<h2 className="text-3xl md:text-4xl font-semibold tracking-tight mb-4 text-white">
|
| 180 |
+
How Candles Work
|
| 181 |
+
</h2>
|
| 182 |
+
<p className="text-neutral-400 max-w-2xl mx-auto">
|
| 183 |
+
A simple three-step cycle that rewards your spiritual growth and empowers your ministry
|
| 184 |
+
</p>
|
| 185 |
+
</div>
|
| 186 |
+
|
| 187 |
+
{/* Three Step Flow */}
|
| 188 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
|
| 189 |
+
{[
|
| 190 |
+
{
|
| 191 |
+
step: '01',
|
| 192 |
+
icon: Sun,
|
| 193 |
+
title: 'Earn',
|
| 194 |
+
subtitle: 'Through Engagement',
|
| 195 |
+
description: 'Complete spiritual activities, engage with your community, and grow in faith to earn Candles.',
|
| 196 |
+
color: 'amber',
|
| 197 |
+
examples: ['Daily devotions', 'Prayer requests', 'Community posts'],
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
step: '02',
|
| 201 |
+
icon: Zap,
|
| 202 |
+
title: 'Spend',
|
| 203 |
+
subtitle: 'On AI Features',
|
| 204 |
+
description: 'Use your Candles to access powerful AI tools that enhance your worship and ministry.',
|
| 205 |
+
color: 'purple',
|
| 206 |
+
examples: ['ORA™ insights', 'Video editing', 'Sermon prep'],
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
step: '03',
|
| 210 |
+
icon: Share2,
|
| 211 |
+
title: 'Share',
|
| 212 |
+
subtitle: 'With Others',
|
| 213 |
+
description: 'Gift Candles to friends, family, or donate to churches and ministries in need.',
|
| 214 |
+
color: 'emerald',
|
| 215 |
+
examples: ['Gift to friends', 'Support churches', 'Fund missions'],
|
| 216 |
+
},
|
| 217 |
+
].map((item, i) => {
|
| 218 |
+
const colorClasses = {
|
| 219 |
+
amber: { bg: 'bg-amber-500/10', border: 'border-amber-500/20', hoverBorder: 'hover:border-amber-500/40', text: 'text-amber-400', spotlight: 'rgba(245, 158, 11, 0.15)' },
|
| 220 |
+
purple: { bg: 'bg-purple-500/10', border: 'border-purple-500/20', hoverBorder: 'hover:border-purple-500/40', text: 'text-purple-400', spotlight: 'rgba(168, 85, 247, 0.15)' },
|
| 221 |
+
emerald: { bg: 'bg-emerald-500/10', border: 'border-emerald-500/20', hoverBorder: 'hover:border-emerald-500/40', text: 'text-emerald-400', spotlight: 'rgba(16, 185, 129, 0.15)' },
|
| 222 |
+
};
|
| 223 |
+
const colors = colorClasses[item.color as keyof typeof colorClasses];
|
| 224 |
+
const staggerClass = `stagger-${i + 1}`;
|
| 225 |
+
|
| 226 |
+
return (
|
| 227 |
+
<SpotlightCard
|
| 228 |
+
key={i}
|
| 229 |
+
className={`p-8 rounded-3xl border ${colors.border} bg-white/[0.02] ${colors.hoverBorder} transition-all group animate-fade-slide-in ${staggerClass} relative overflow-hidden`}
|
| 230 |
+
spotlightColor={colors.spotlight}
|
| 231 |
+
>
|
| 232 |
+
{/* Step Number */}
|
| 233 |
+
<div className="absolute top-4 right-4 text-6xl font-bold text-white/5">{item.step}</div>
|
| 234 |
+
|
| 235 |
+
<div className={`w-16 h-16 rounded-2xl ${colors.bg} flex items-center justify-center mb-6 border ${colors.border} group-hover:scale-110 transition-transform`}>
|
| 236 |
+
<item.icon className={`w-8 h-8 ${colors.text}`} />
|
| 237 |
+
</div>
|
| 238 |
+
|
| 239 |
+
<div className="mb-2">
|
| 240 |
+
<h3 className="text-2xl font-semibold text-white">{item.title}</h3>
|
| 241 |
+
<p className={`text-sm ${colors.text}`}>{item.subtitle}</p>
|
| 242 |
+
</div>
|
| 243 |
+
|
| 244 |
+
<p className="text-neutral-400 text-sm leading-relaxed mb-6">{item.description}</p>
|
| 245 |
+
|
| 246 |
+
<div className="space-y-2">
|
| 247 |
+
{item.examples.map((ex, j) => (
|
| 248 |
+
<div key={j} className="flex items-center gap-2 text-xs text-neutral-500">
|
| 249 |
+
<Check className={`w-3.5 h-3.5 ${colors.text}`} />
|
| 250 |
+
{ex}
|
| 251 |
+
</div>
|
| 252 |
+
))}
|
| 253 |
+
</div>
|
| 254 |
+
</SpotlightCard>
|
| 255 |
+
);
|
| 256 |
+
})}
|
| 257 |
+
</div>
|
| 258 |
+
|
| 259 |
+
{/* Connection Line */}
|
| 260 |
+
<div className="hidden md:flex items-center justify-center gap-4 -mt-8 mb-8">
|
| 261 |
+
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-500/30 to-transparent" />
|
| 262 |
+
<div className="w-3 h-3 rounded-full bg-amber-500/30 border border-amber-500/50" />
|
| 263 |
+
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-purple-500/30 to-transparent" />
|
| 264 |
+
<div className="w-3 h-3 rounded-full bg-purple-500/30 border border-purple-500/50" />
|
| 265 |
+
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-emerald-500/30 to-transparent" />
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
</section>
|
| 269 |
+
|
| 270 |
+
{/* Live Candle Counter Demo */}
|
| 271 |
+
<section className="py-24 relative border-t border-white/5 bg-[#080808]">
|
| 272 |
+
<div className="max-w-7xl mx-auto px-6">
|
| 273 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
| 274 |
+
{/* Left: Demo Interface */}
|
| 275 |
+
<div className="rounded-3xl border border-white/10 bg-[#0a0a0a] overflow-hidden">
|
| 276 |
+
<div className="px-6 py-4 border-b border-white/5 flex items-center justify-between bg-white/[0.02]">
|
| 277 |
+
<div className="flex items-center gap-2">
|
| 278 |
+
<div className="w-3 h-3 rounded-full bg-red-500/50" />
|
| 279 |
+
<div className="w-3 h-3 rounded-full bg-yellow-500/50" />
|
| 280 |
+
<div className="w-3 h-3 rounded-full bg-green-500/50" />
|
| 281 |
+
</div>
|
| 282 |
+
<span className="text-xs text-white/30 font-mono">candle-wallet.app</span>
|
| 283 |
+
</div>
|
| 284 |
+
|
| 285 |
+
<div className="p-8">
|
| 286 |
+
{/* Balance Display */}
|
| 287 |
+
<div className="text-center mb-8">
|
| 288 |
+
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-2">Your Candle Balance</div>
|
| 289 |
+
<div className="flex items-center justify-center gap-3">
|
| 290 |
+
<Flame className="w-10 h-10 text-amber-400 animate-flicker" />
|
| 291 |
+
<span className="text-6xl font-bold text-white">1,247</span>
|
| 292 |
+
</div>
|
| 293 |
+
<div className="text-sm text-emerald-400 mt-2">+127 this week</div>
|
| 294 |
+
</div>
|
| 295 |
+
|
| 296 |
+
{/* Recent Activity */}
|
| 297 |
+
<div className="space-y-3">
|
| 298 |
+
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-3">Recent Activity</div>
|
| 299 |
+
{[
|
| 300 |
+
{ action: 'Completed Daily Devotion', amount: '+25', icon: BookOpen, color: 'amber', time: '2h ago' },
|
| 301 |
+
{ action: 'Posted Prayer Request', amount: '+10', icon: Heart, color: 'rose', time: '5h ago' },
|
| 302 |
+
{ action: 'Used ORA™ for Bible Study', amount: '-15', icon: Sparkles, color: 'purple', time: '1d ago' },
|
| 303 |
+
{ action: '7-Day Streak Bonus!', amount: '+50', icon: Trophy, color: 'emerald', time: '1d ago' },
|
| 304 |
+
].map((item, i) => (
|
| 305 |
+
<div key={i} className="flex items-center justify-between p-3 rounded-xl bg-white/5 border border-white/5">
|
| 306 |
+
<div className="flex items-center gap-3">
|
| 307 |
+
<div className={`w-8 h-8 rounded-lg bg-${item.color}-500/10 flex items-center justify-center`}>
|
| 308 |
+
<item.icon className={`w-4 h-4 text-${item.color}-400`} />
|
| 309 |
+
</div>
|
| 310 |
+
<div>
|
| 311 |
+
<div className="text-sm text-white">{item.action}</div>
|
| 312 |
+
<div className="text-[10px] text-neutral-500">{item.time}</div>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
<span className={`text-sm font-semibold ${item.amount.startsWith('+') ? 'text-emerald-400' : 'text-amber-400'}`}>
|
| 316 |
+
{item.amount}
|
| 317 |
+
</span>
|
| 318 |
+
</div>
|
| 319 |
+
))}
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
|
| 324 |
+
{/* Right: Content */}
|
| 325 |
+
<div>
|
| 326 |
+
<h2 className="text-3xl md:text-4xl font-semibold tracking-tight mb-6 text-white">
|
| 327 |
+
Watch Your Candles Grow
|
| 328 |
+
</h2>
|
| 329 |
+
<p className="text-neutral-400 leading-relaxed mb-8">
|
| 330 |
+
Every spiritual activity earns you Candles. Track your balance, see your growth over time, and celebrate milestones in your faith journey.
|
| 331 |
+
</p>
|
| 332 |
+
|
| 333 |
+
<div className="space-y-4 mb-8">
|
| 334 |
+
{[
|
| 335 |
+
{ label: 'Real-time balance updates', desc: 'See your Candles change as you engage' },
|
| 336 |
+
{ label: 'Activity history', desc: 'Track every earn and spend transaction' },
|
| 337 |
+
{ label: 'Streak bonuses', desc: 'Earn bonus Candles for consistent engagement' },
|
| 338 |
+
{ label: 'Monthly refresh', desc: 'Get 250 free Candles every month' },
|
| 339 |
+
].map((item, i) => (
|
| 340 |
+
<div key={i} className="flex items-start gap-3">
|
| 341 |
+
<div className="w-6 h-6 rounded-full bg-amber-500/20 flex items-center justify-center shrink-0 mt-0.5">
|
| 342 |
+
<Check className="w-3.5 h-3.5 text-amber-400" />
|
| 343 |
+
</div>
|
| 344 |
+
<div>
|
| 345 |
+
<div className="text-white font-medium">{item.label}</div>
|
| 346 |
+
<div className="text-sm text-neutral-500">{item.desc}</div>
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
))}
|
| 350 |
+
</div>
|
| 351 |
+
|
| 352 |
+
<Link href="/get-started">
|
| 353 |
+
<ShinyButton>
|
| 354 |
+
<span className="flex items-center gap-2">
|
| 355 |
+
Start Earning Today
|
| 356 |
+
<ArrowRight className="w-4 h-4" />
|
| 357 |
+
</span>
|
| 358 |
+
</ShinyButton>
|
| 359 |
+
</Link>
|
| 360 |
+
</div>
|
| 361 |
+
</div>
|
| 362 |
+
</div>
|
| 363 |
+
</section>
|
| 364 |
+
|
| 365 |
+
{/* Earn Candles Section - Enhanced */}
|
| 366 |
+
<section className="py-24 relative border-t border-white/5">
|
| 367 |
+
<div className="max-w-7xl mx-auto px-6">
|
| 368 |
+
<div className="text-center mb-16">
|
| 369 |
+
<h2 className="text-3xl md:text-4xl font-semibold tracking-tight mb-4 text-white">
|
| 370 |
+
Ways to Earn Candles
|
| 371 |
+
</h2>
|
| 372 |
+
<p className="text-neutral-400 max-w-lg mx-auto">
|
| 373 |
+
Every meaningful interaction with your faith community earns you Candles
|
| 374 |
+
</p>
|
| 375 |
+
</div>
|
| 376 |
+
|
| 377 |
+
{/* Community Engagement */}
|
| 378 |
+
<div className="mb-12">
|
| 379 |
+
<div className="flex items-center gap-3 mb-6">
|
| 380 |
+
<div className="w-10 h-10 rounded-xl bg-blue-500/10 flex items-center justify-center border border-blue-500/20">
|
| 381 |
+
<MessageSquare className="w-5 h-5 text-blue-400" />
|
| 382 |
+
</div>
|
| 383 |
+
<div>
|
| 384 |
+
<h3 className="text-lg font-semibold text-white">Community Engagement</h3>
|
| 385 |
+
<p className="text-xs text-neutral-500">Connect and interact with your faith family</p>
|
| 386 |
+
</div>
|
| 387 |
+
</div>
|
| 388 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
| 389 |
+
{[
|
| 390 |
+
{ icon: UserPlus, title: 'Add A Contact', candles: 25, badge: 'Connector', color: 'blue' },
|
| 391 |
+
{ icon: Target, title: 'Attend An Event', candles: 25, badge: 'Active Member', color: 'blue' },
|
| 392 |
+
{ icon: MessageSquare, title: 'Post A Discussion', candles: 20, badge: 'Community Builder', color: 'blue' },
|
| 393 |
+
{ icon: ThumbsUp, title: 'Like/Amen/Share', candles: 5, badge: 'Encourager', color: 'blue' },
|
| 394 |
+
].map((item, i) => {
|
| 395 |
+
const staggerClass = `stagger-${(i % 4) + 1}`;
|
| 396 |
+
return (
|
| 397 |
+
<SpotlightCard key={i} className={`p-5 rounded-2xl border border-blue-500/20 bg-blue-500/5 hover:border-blue-500/40 transition-all group animate-fade-slide-in ${staggerClass}`} spotlightColor="rgba(59, 130, 246, 0.15)">
|
| 398 |
+
<div className="flex items-center justify-between mb-3">
|
| 399 |
+
<div className="w-10 h-10 rounded-xl bg-blue-500/20 flex items-center justify-center group-hover:scale-110 transition-transform">
|
| 400 |
+
<item.icon className="w-5 h-5 text-blue-400" />
|
| 401 |
+
</div>
|
| 402 |
+
<span className="text-[10px] px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-300 border border-blue-500/30">{item.badge}</span>
|
| 403 |
+
</div>
|
| 404 |
+
<h4 className="text-sm font-medium text-white mb-1">{item.title}</h4>
|
| 405 |
+
<div className="flex items-center gap-1">
|
| 406 |
+
<Flame className="w-4 h-4 text-amber-400" />
|
| 407 |
+
<span className="text-amber-400 font-bold">{item.candles}</span>
|
| 408 |
+
<span className="text-neutral-500 text-xs">Candles</span>
|
| 409 |
+
</div>
|
| 410 |
+
</SpotlightCard>
|
| 411 |
+
);
|
| 412 |
+
})}
|
| 413 |
+
</div>
|
| 414 |
+
</div>
|
| 415 |
+
|
| 416 |
+
{/* Spiritual Habits */}
|
| 417 |
+
<div className="mb-12">
|
| 418 |
+
<div className="flex items-center gap-3 mb-6">
|
| 419 |
+
<div className="w-10 h-10 rounded-xl bg-rose-500/10 flex items-center justify-center border border-rose-500/20">
|
| 420 |
+
<Heart className="w-5 h-5 text-rose-400" />
|
| 421 |
+
</div>
|
| 422 |
+
<div>
|
| 423 |
+
<h3 className="text-lg font-semibold text-white">Spiritual Habits</h3>
|
| 424 |
+
<p className="text-xs text-neutral-500">Deepen your faith through daily practices</p>
|
| 425 |
+
</div>
|
| 426 |
+
</div>
|
| 427 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
| 428 |
+
{[
|
| 429 |
+
{ icon: Trophy, title: 'Finish A Reading Plan', candles: 50, badge: 'Devoted Reader', color: 'rose' },
|
| 430 |
+
{ icon: Flame, title: '7-Day Streak', candles: 50, badge: 'Disciplined', color: 'rose' },
|
| 431 |
+
{ icon: BookOpen, title: 'Post S.O.A.P. Journal', candles: 25, badge: 'Reflective', color: 'rose' },
|
| 432 |
+
{ icon: Heart, title: 'Request A Prayer', candles: 10, badge: 'Prayer Warrior', color: 'rose' },
|
| 433 |
+
].map((item, i) => {
|
| 434 |
+
const staggerClass = `stagger-${(i % 4) + 1}`;
|
| 435 |
+
return (
|
| 436 |
+
<SpotlightCard key={i} className={`p-5 rounded-2xl border border-rose-500/20 bg-rose-500/5 hover:border-rose-500/40 transition-all group animate-fade-slide-in ${staggerClass}`} spotlightColor="rgba(244, 63, 94, 0.15)">
|
| 437 |
+
<div className="flex items-center justify-between mb-3">
|
| 438 |
+
<div className="w-10 h-10 rounded-xl bg-rose-500/20 flex items-center justify-center group-hover:scale-110 transition-transform">
|
| 439 |
+
<item.icon className="w-5 h-5 text-rose-400" />
|
| 440 |
+
</div>
|
| 441 |
+
<span className="text-[10px] px-2 py-0.5 rounded-full bg-rose-500/20 text-rose-300 border border-rose-500/30">{item.badge}</span>
|
| 442 |
+
</div>
|
| 443 |
+
<h4 className="text-sm font-medium text-white mb-1">{item.title}</h4>
|
| 444 |
+
<div className="flex items-center gap-1">
|
| 445 |
+
<Flame className="w-4 h-4 text-amber-400" />
|
| 446 |
+
<span className="text-amber-400 font-bold">{item.candles}</span>
|
| 447 |
+
<span className="text-neutral-500 text-xs">Candles</span>
|
| 448 |
+
</div>
|
| 449 |
+
</SpotlightCard>
|
| 450 |
+
);
|
| 451 |
+
})}
|
| 452 |
+
</div>
|
| 453 |
+
</div>
|
| 454 |
+
|
| 455 |
+
{/* Growth & Leadership */}
|
| 456 |
+
<div>
|
| 457 |
+
<div className="flex items-center gap-3 mb-6">
|
| 458 |
+
<div className="w-10 h-10 rounded-xl bg-amber-500/10 flex items-center justify-center border border-amber-500/20">
|
| 459 |
+
<Star className="w-5 h-5 text-amber-400" />
|
| 460 |
+
</div>
|
| 461 |
+
<div>
|
| 462 |
+
<h3 className="text-lg font-semibold text-white">Growth & Leadership</h3>
|
| 463 |
+
<p className="text-xs text-neutral-500">Big rewards for big commitments</p>
|
| 464 |
+
</div>
|
| 465 |
+
</div>
|
| 466 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
| 467 |
+
{[
|
| 468 |
+
{ icon: Users, title: 'Complete Profile', candles: 250, badge: 'Committed', highlight: true },
|
| 469 |
+
{ icon: Gift, title: 'Refer A Friend', candles: 250, badge: 'Evangelist', highlight: true },
|
| 470 |
+
{ icon: Church, title: 'Create A Church', candles: 100, badge: 'Church Builder', highlight: false },
|
| 471 |
+
{ icon: Users, title: 'Create A Ministry', candles: 50, badge: 'Founder', highlight: false },
|
| 472 |
+
].map((item, i) => {
|
| 473 |
+
const staggerClass = `stagger-${(i % 4) + 1}`;
|
| 474 |
+
return (
|
| 475 |
+
<SpotlightCard
|
| 476 |
+
key={i}
|
| 477 |
+
className={`p-5 rounded-2xl border ${item.highlight ? 'border-amber-500/30 bg-amber-500/10' : 'border-emerald-500/20 bg-emerald-500/5'} ${item.highlight ? 'hover:border-amber-500/50' : 'hover:border-emerald-500/40'} transition-all group animate-fade-slide-in ${staggerClass}`}
|
| 478 |
+
spotlightColor={item.highlight ? "rgba(245, 158, 11, 0.2)" : "rgba(16, 185, 129, 0.15)"}
|
| 479 |
+
>
|
| 480 |
+
{item.highlight && (
|
| 481 |
+
<div className="absolute top-2 right-2">
|
| 482 |
+
<span className="text-[8px] px-1.5 py-0.5 rounded bg-amber-500/30 text-amber-200 font-bold uppercase">Bonus</span>
|
| 483 |
+
</div>
|
| 484 |
+
)}
|
| 485 |
+
<div className="flex items-center justify-between mb-3">
|
| 486 |
+
<div className={`w-10 h-10 rounded-xl ${item.highlight ? 'bg-amber-500/20' : 'bg-emerald-500/20'} flex items-center justify-center group-hover:scale-110 transition-transform`}>
|
| 487 |
+
<item.icon className={`w-5 h-5 ${item.highlight ? 'text-amber-400' : 'text-emerald-400'}`} />
|
| 488 |
+
</div>
|
| 489 |
+
<span className={`text-[10px] px-2 py-0.5 rounded-full ${item.highlight ? 'bg-amber-500/20 text-amber-300 border-amber-500/30' : 'bg-emerald-500/20 text-emerald-300 border-emerald-500/30'} border`}>{item.badge}</span>
|
| 490 |
+
</div>
|
| 491 |
+
<h4 className="text-sm font-medium text-white mb-1">{item.title}</h4>
|
| 492 |
+
<div className="flex items-center gap-1">
|
| 493 |
+
<Flame className="w-4 h-4 text-amber-400" />
|
| 494 |
+
<span className="text-amber-400 font-bold text-lg">{item.candles}</span>
|
| 495 |
+
<span className="text-neutral-500 text-xs">Candles</span>
|
| 496 |
+
</div>
|
| 497 |
+
</SpotlightCard>
|
| 498 |
+
);
|
| 499 |
+
})}
|
| 500 |
+
</div>
|
| 501 |
+
</div>
|
| 502 |
+
|
| 503 |
+
<p className="text-center text-xs text-neutral-500 mt-8">
|
| 504 |
+
*Candles are awarded upon completion. Maximum 5,000 Candles per person per month.
|
| 505 |
+
</p>
|
| 506 |
+
</div>
|
| 507 |
+
</section>
|
| 508 |
+
|
| 509 |
+
{/* Spend Your Candles */}
|
| 510 |
+
<section className="py-24 relative border-t border-white/5 bg-[#080808]">
|
| 511 |
+
<div className="max-w-7xl mx-auto px-6">
|
| 512 |
+
<div className="text-center mb-16">
|
| 513 |
+
<h2 className="text-3xl md:text-4xl font-semibold tracking-tight mb-4 text-white">
|
| 514 |
+
Spend Your Candles
|
| 515 |
+
</h2>
|
| 516 |
+
<p className="text-neutral-400 max-w-lg mx-auto">
|
| 517 |
+
Unlock powerful AI features that enhance your worship, study, and ministry
|
| 518 |
+
</p>
|
| 519 |
+
</div>
|
| 520 |
+
|
| 521 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
| 522 |
+
{[
|
| 523 |
+
{ icon: Sparkles, title: 'ORA™ Insights', desc: 'AI-powered Scripture analysis and spiritual guidance', cost: '5-15', color: 'purple' },
|
| 524 |
+
{ icon: Video, title: 'Pro Studio', desc: 'Professional video editing and content creation', cost: '10-50', color: 'amber' },
|
| 525 |
+
{ icon: BookOpen, title: 'Study Tools', desc: 'Advanced Bible study and sermon preparation', cost: '5-20', color: 'blue' },
|
| 526 |
+
{ icon: MessageSquare, title: 'Content AI', desc: 'Social media posts and ministry content', cost: '5-25', color: 'emerald' },
|
| 527 |
+
].map((item, i) => {
|
| 528 |
+
const colorClasses = {
|
| 529 |
+
purple: { bg: 'bg-purple-500/10', border: 'border-purple-500/20', hoverBorder: 'hover:border-purple-500/40', text: 'text-purple-400' },
|
| 530 |
+
amber: { bg: 'bg-amber-500/10', border: 'border-amber-500/20', hoverBorder: 'hover:border-amber-500/40', text: 'text-amber-400' },
|
| 531 |
+
blue: { bg: 'bg-blue-500/10', border: 'border-blue-500/20', hoverBorder: 'hover:border-blue-500/40', text: 'text-blue-400' },
|
| 532 |
+
emerald: { bg: 'bg-emerald-500/10', border: 'border-emerald-500/20', hoverBorder: 'hover:border-emerald-500/40', text: 'text-emerald-400' },
|
| 533 |
+
};
|
| 534 |
+
const colors = colorClasses[item.color as keyof typeof colorClasses];
|
| 535 |
+
const staggerClass = `stagger-${(i % 4) + 1}`;
|
| 536 |
+
|
| 537 |
+
return (
|
| 538 |
+
<SpotlightCard key={i} className={`p-6 rounded-2xl border ${colors.border} bg-white/[0.02] ${colors.hoverBorder} transition-all group animate-fade-slide-in ${staggerClass}`}>
|
| 539 |
+
<div className={`w-14 h-14 rounded-xl ${colors.bg} flex items-center justify-center mb-4 border ${colors.border} group-hover:scale-110 transition-transform`}>
|
| 540 |
+
<item.icon className={`w-7 h-7 ${colors.text}`} />
|
| 541 |
+
</div>
|
| 542 |
+
<h3 className="text-lg font-semibold text-white mb-2">{item.title}</h3>
|
| 543 |
+
<p className="text-neutral-400 text-sm leading-relaxed mb-4">{item.desc}</p>
|
| 544 |
+
<div className="flex items-center gap-2 text-sm">
|
| 545 |
+
<Flame className="w-4 h-4 text-amber-400" />
|
| 546 |
+
<span className="text-amber-400 font-semibold">{item.cost}</span>
|
| 547 |
+
<span className="text-neutral-500">Candles per use</span>
|
| 548 |
+
</div>
|
| 549 |
+
</SpotlightCard>
|
| 550 |
+
);
|
| 551 |
+
})}
|
| 552 |
+
</div>
|
| 553 |
+
</div>
|
| 554 |
+
</section>
|
| 555 |
+
|
| 556 |
+
{/* Share the Light */}
|
| 557 |
+
<section className="py-24 relative border-t border-white/5">
|
| 558 |
+
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[600px] h-[400px] bg-amber-600/5 rounded-full blur-[100px] pointer-events-none -z-10" />
|
| 559 |
+
|
| 560 |
+
<div className="max-w-7xl mx-auto px-6">
|
| 561 |
+
<div className="text-center mb-16">
|
| 562 |
+
<h2 className="text-3xl md:text-4xl font-semibold tracking-tight mb-4 text-white">
|
| 563 |
+
Share the Light
|
| 564 |
+
</h2>
|
| 565 |
+
<p className="text-neutral-400 max-w-lg mx-auto">
|
| 566 |
+
Gift your Candles to bless others in their faith journey
|
| 567 |
+
</p>
|
| 568 |
+
</div>
|
| 569 |
+
|
| 570 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
| 571 |
+
{[
|
| 572 |
+
{ icon: Users, title: 'Gift to Friends', desc: 'Send Candles to friends and family who need AI features for their spiritual growth', color: 'blue' },
|
| 573 |
+
{ icon: Church, title: 'Support Churches', desc: 'Donate to churches and help them access powerful ministry tools', color: 'purple' },
|
| 574 |
+
{ icon: Heart, title: 'Fund Missions', desc: 'Empower missionaries and ministry teams with collaborative Candle pools', color: 'emerald' },
|
| 575 |
+
].map((item, i) => {
|
| 576 |
+
const colorClasses = {
|
| 577 |
+
blue: { bg: 'bg-blue-500/10', border: 'border-blue-500/20', hoverBorder: 'hover:border-blue-500/40', text: 'text-blue-400', spotlight: 'rgba(59, 130, 246, 0.15)' },
|
| 578 |
+
purple: { bg: 'bg-purple-500/10', border: 'border-purple-500/20', hoverBorder: 'hover:border-purple-500/40', text: 'text-purple-400', spotlight: 'rgba(168, 85, 247, 0.15)' },
|
| 579 |
+
emerald: { bg: 'bg-emerald-500/10', border: 'border-emerald-500/20', hoverBorder: 'hover:border-emerald-500/40', text: 'text-emerald-400', spotlight: 'rgba(16, 185, 129, 0.15)' },
|
| 580 |
+
};
|
| 581 |
+
const colors = colorClasses[item.color as keyof typeof colorClasses];
|
| 582 |
+
const staggerClass = `stagger-${i + 1}`;
|
| 583 |
+
|
| 584 |
+
return (
|
| 585 |
+
<SpotlightCard key={i} className={`p-8 rounded-3xl border ${colors.border} bg-white/[0.02] ${colors.hoverBorder} transition-all group text-center animate-fade-slide-in ${staggerClass}`} spotlightColor={colors.spotlight}>
|
| 586 |
+
<div className={`w-16 h-16 mx-auto rounded-2xl ${colors.bg} flex items-center justify-center mb-6 border ${colors.border} group-hover:scale-110 transition-transform`}>
|
| 587 |
+
<item.icon className={`w-8 h-8 ${colors.text}`} />
|
| 588 |
+
</div>
|
| 589 |
+
<h3 className="text-lg font-semibold text-white mb-3">{item.title}</h3>
|
| 590 |
+
<p className="text-neutral-400 text-sm leading-relaxed">{item.desc}</p>
|
| 591 |
+
</SpotlightCard>
|
| 592 |
+
);
|
| 593 |
+
})}
|
| 594 |
+
</div>
|
| 595 |
+
</div>
|
| 596 |
+
</section>
|
| 597 |
+
|
| 598 |
+
{/* CTA Section */}
|
| 599 |
+
<section className="py-32 text-center relative overflow-hidden border-t border-white/5 bg-[#080808]">
|
| 600 |
+
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px] bg-amber-600/10 rounded-full blur-[120px] pointer-events-none -z-10" />
|
| 601 |
+
|
| 602 |
+
<div className="max-w-3xl mx-auto px-6 relative z-10">
|
| 603 |
+
<div className="w-24 h-24 mx-auto rounded-full bg-gradient-to-br from-amber-500/20 to-orange-600/10 flex items-center justify-center mb-8 border border-amber-500/30 animate-breathe shadow-[0_0_60px_-15px_rgba(251,191,36,0.5)]">
|
| 604 |
+
<Flame className="w-12 h-12 text-amber-400 drop-shadow-[0_0_20px_rgba(251,191,36,0.8)]" />
|
| 605 |
+
</div>
|
| 606 |
+
|
| 607 |
+
<h2 className="text-4xl sm:text-5xl font-semibold tracking-tight mb-6 text-white">
|
| 608 |
+
Start Your Candle Journey
|
| 609 |
+
</h2>
|
| 610 |
+
<p className="text-neutral-400 text-lg mb-10 max-w-xl mx-auto">
|
| 611 |
+
Join thousands of believers earning and using Candles to enhance their faith journey with AI-powered tools.
|
| 612 |
+
</p>
|
| 613 |
+
|
| 614 |
+
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
| 615 |
+
<Link href="/get-started">
|
| 616 |
+
<ShinyButton>
|
| 617 |
+
<span className="flex items-center gap-2">
|
| 618 |
+
<Flame className="w-[18px] h-[18px] candle-glow" />
|
| 619 |
+
Get 250 Free Candles
|
| 620 |
+
</span>
|
| 621 |
+
</ShinyButton>
|
| 622 |
+
</Link>
|
| 623 |
+
<Link href="/pricing" className="px-8 py-3.5 text-sm font-medium text-neutral-300 border border-white/10 rounded-full hover:bg-white/5 hover:text-white transition-all">
|
| 624 |
+
View Pricing Plans
|
| 625 |
+
</Link>
|
| 626 |
+
</div>
|
| 627 |
+
|
| 628 |
+
<p className="text-xs text-neutral-500 mt-6">
|
| 629 |
+
No credit card required • 250 Candles refresh monthly
|
| 630 |
+
</p>
|
| 631 |
+
</div>
|
| 632 |
+
</section>
|
| 633 |
+
</>
|
| 634 |
+
);
|
| 635 |
+
}
|
frontend/app/contact/page.tsx
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { Mail, MessageSquare, MapPin, Send, Sparkles } from 'lucide-react';
|
| 4 |
+
import SpotlightCard from '@/components/SpotlightCard';
|
| 5 |
+
import ShinyButton from '@/components/ShinyButton';
|
| 6 |
+
import Link from 'next/link';
|
| 7 |
+
|
| 8 |
+
export default function ContactPage() {
|
| 9 |
+
return (
|
| 10 |
+
<>
|
| 11 |
+
{/* Hero */}
|
| 12 |
+
<section className="relative pt-32 pb-20 overflow-hidden">
|
| 13 |
+
<div className="absolute inset-0 ora-grid-bg pointer-events-none z-0" />
|
| 14 |
+
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[600px] bg-purple-600/20 rounded-full blur-[150px] pointer-events-none -z-10" />
|
| 15 |
+
|
| 16 |
+
<div className="max-w-4xl mx-auto px-6 text-center">
|
| 17 |
+
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-purple-500/30 bg-purple-500/10 mb-8">
|
| 18 |
+
<Mail className="w-4 h-4 text-purple-400" />
|
| 19 |
+
<span className="text-xs font-mono text-purple-300 uppercase tracking-wider">Contact</span>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<h1 className="text-4xl md:text-5xl font-bold tracking-tight mb-6 text-white">
|
| 23 |
+
Get in <span className="text-purple-400">Touch</span>
|
| 24 |
+
</h1>
|
| 25 |
+
|
| 26 |
+
<p className="text-lg text-neutral-400 max-w-2xl mx-auto">
|
| 27 |
+
Have questions about ORA? We'd love to hear from you. Reach out and we'll respond as soon as we can.
|
| 28 |
+
</p>
|
| 29 |
+
</div>
|
| 30 |
+
</section>
|
| 31 |
+
|
| 32 |
+
{/* Contact Methods */}
|
| 33 |
+
<section className="py-16 border-t border-white/5">
|
| 34 |
+
<div className="max-w-4xl mx-auto px-6">
|
| 35 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
|
| 36 |
+
<SpotlightCard className="p-6 rounded-xl border border-purple-500/20 bg-white/[0.02]" spotlightColor="purple">
|
| 37 |
+
<div className="w-12 h-12 rounded-xl bg-purple-500/10 flex items-center justify-center border border-purple-500/20 mb-4">
|
| 38 |
+
<Mail className="w-6 h-6 text-purple-400" />
|
| 39 |
+
</div>
|
| 40 |
+
<h3 className="text-lg font-semibold text-white mb-2">Email</h3>
|
| 41 |
+
<p className="text-neutral-500 text-sm mb-3">Send us an email anytime</p>
|
| 42 |
+
<a href="mailto:support@ora.ai" className="text-purple-400 hover:text-purple-300 transition-colors text-sm">
|
| 43 |
+
support@ora.ai
|
| 44 |
+
</a>
|
| 45 |
+
</SpotlightCard>
|
| 46 |
+
|
| 47 |
+
<SpotlightCard className="p-6 rounded-xl border border-blue-500/20 bg-white/[0.02]" spotlightColor="purple">
|
| 48 |
+
<div className="w-12 h-12 rounded-xl bg-blue-500/10 flex items-center justify-center border border-blue-500/20 mb-4">
|
| 49 |
+
<MessageSquare className="w-6 h-6 text-blue-400" />
|
| 50 |
+
</div>
|
| 51 |
+
<h3 className="text-lg font-semibold text-white mb-2">Chat with ORA</h3>
|
| 52 |
+
<p className="text-neutral-500 text-sm mb-3">Try our AI assistant directly</p>
|
| 53 |
+
<Link href="/dashboard" className="text-blue-400 hover:text-blue-300 transition-colors text-sm">
|
| 54 |
+
Open Dashboard
|
| 55 |
+
</Link>
|
| 56 |
+
</SpotlightCard>
|
| 57 |
+
|
| 58 |
+
<SpotlightCard className="p-6 rounded-xl border border-emerald-500/20 bg-white/[0.02]" spotlightColor="purple">
|
| 59 |
+
<div className="w-12 h-12 rounded-xl bg-emerald-500/10 flex items-center justify-center border border-emerald-500/20 mb-4">
|
| 60 |
+
<Sparkles className="w-6 h-6 text-emerald-400" />
|
| 61 |
+
</div>
|
| 62 |
+
<h3 className="text-lg font-semibold text-white mb-2">Documentation</h3>
|
| 63 |
+
<p className="text-neutral-500 text-sm mb-3">Browse our help resources</p>
|
| 64 |
+
<Link href="/help" className="text-emerald-400 hover:text-emerald-300 transition-colors text-sm">
|
| 65 |
+
View Help Center
|
| 66 |
+
</Link>
|
| 67 |
+
</SpotlightCard>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
{/* Contact Form */}
|
| 71 |
+
<div className="max-w-xl mx-auto">
|
| 72 |
+
<h2 className="text-2xl font-bold text-white text-center mb-8">Send a Message</h2>
|
| 73 |
+
<form className="space-y-4">
|
| 74 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
| 75 |
+
<div>
|
| 76 |
+
<label className="block text-sm text-neutral-400 mb-2">Name</label>
|
| 77 |
+
<input
|
| 78 |
+
type="text"
|
| 79 |
+
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-neutral-500 focus:outline-none focus:border-purple-500/50 transition-colors"
|
| 80 |
+
placeholder="Your name"
|
| 81 |
+
/>
|
| 82 |
+
</div>
|
| 83 |
+
<div>
|
| 84 |
+
<label className="block text-sm text-neutral-400 mb-2">Email</label>
|
| 85 |
+
<input
|
| 86 |
+
type="email"
|
| 87 |
+
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-neutral-500 focus:outline-none focus:border-purple-500/50 transition-colors"
|
| 88 |
+
placeholder="you@example.com"
|
| 89 |
+
/>
|
| 90 |
+
</div>
|
| 91 |
+
</div>
|
| 92 |
+
<div>
|
| 93 |
+
<label className="block text-sm text-neutral-400 mb-2">Subject</label>
|
| 94 |
+
<input
|
| 95 |
+
type="text"
|
| 96 |
+
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-neutral-500 focus:outline-none focus:border-purple-500/50 transition-colors"
|
| 97 |
+
placeholder="How can we help?"
|
| 98 |
+
/>
|
| 99 |
+
</div>
|
| 100 |
+
<div>
|
| 101 |
+
<label className="block text-sm text-neutral-400 mb-2">Message</label>
|
| 102 |
+
<textarea
|
| 103 |
+
rows={5}
|
| 104 |
+
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-neutral-500 focus:outline-none focus:border-purple-500/50 transition-colors resize-none"
|
| 105 |
+
placeholder="Tell us more..."
|
| 106 |
+
/>
|
| 107 |
+
</div>
|
| 108 |
+
<button
|
| 109 |
+
type="submit"
|
| 110 |
+
className="w-full py-3 rounded-xl bg-purple-600 hover:bg-purple-500 text-white font-medium transition-colors flex items-center justify-center gap-2"
|
| 111 |
+
>
|
| 112 |
+
<Send className="w-4 h-4" />
|
| 113 |
+
Send Message
|
| 114 |
+
</button>
|
| 115 |
+
</form>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
</section>
|
| 119 |
+
</>
|
| 120 |
+
);
|
| 121 |
+
}
|
frontend/app/dashboard/bible/page.tsx
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import { BookOpen, Search, Bookmark, ChevronRight, Sparkles, History, ExternalLink } from 'lucide-react';
|
| 5 |
+
|
| 6 |
+
const popularBooks = [
|
| 7 |
+
{ name: 'Genesis', chapters: 50, category: 'Torah' },
|
| 8 |
+
{ name: 'Psalms', chapters: 150, category: 'Wisdom' },
|
| 9 |
+
{ name: 'Proverbs', chapters: 31, category: 'Wisdom' },
|
| 10 |
+
{ name: 'Isaiah', chapters: 66, category: 'Prophets' },
|
| 11 |
+
{ name: 'Matthew', chapters: 28, category: 'Gospel' },
|
| 12 |
+
{ name: 'John', chapters: 21, category: 'Gospel' },
|
| 13 |
+
{ name: 'Romans', chapters: 16, category: 'Epistle' },
|
| 14 |
+
{ name: 'Revelation', chapters: 22, category: 'Apocalyptic' },
|
| 15 |
+
];
|
| 16 |
+
|
| 17 |
+
const recentSearches = [
|
| 18 |
+
'Love one another - John 13:34',
|
| 19 |
+
'Faith without works - James 2:17',
|
| 20 |
+
'The Lord is my shepherd - Psalm 23',
|
| 21 |
+
'For God so loved - John 3:16',
|
| 22 |
+
];
|
| 23 |
+
|
| 24 |
+
export default function BiblePage() {
|
| 25 |
+
const [searchQuery, setSearchQuery] = useState('');
|
| 26 |
+
|
| 27 |
+
return (
|
| 28 |
+
<div className="flex-1 overflow-y-auto">
|
| 29 |
+
{/* Header */}
|
| 30 |
+
<div className="sticky top-0 z-10 bg-[#0a0a0a]/90 backdrop-blur-xl border-b border-white/5">
|
| 31 |
+
<div className="max-w-4xl mx-auto px-6 py-4">
|
| 32 |
+
<div className="flex items-center gap-3 mb-4">
|
| 33 |
+
<div className="w-10 h-10 rounded-xl bg-blue-500/10 border border-blue-500/30 flex items-center justify-center">
|
| 34 |
+
<BookOpen className="w-5 h-5 text-blue-400" />
|
| 35 |
+
</div>
|
| 36 |
+
<div>
|
| 37 |
+
<h1 className="text-xl font-semibold text-white">Bible Study</h1>
|
| 38 |
+
<p className="text-sm text-neutral-500">Search Scripture with the Theologian Agent</p>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
{/* Search Bar */}
|
| 43 |
+
<div className="relative">
|
| 44 |
+
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-neutral-500" />
|
| 45 |
+
<input
|
| 46 |
+
type="text"
|
| 47 |
+
value={searchQuery}
|
| 48 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 49 |
+
placeholder="Search verses, topics, or ask a theological question..."
|
| 50 |
+
className="w-full pl-12 pr-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-neutral-500 focus:outline-none focus:border-blue-500/50 transition-colors"
|
| 51 |
+
/>
|
| 52 |
+
<button className="absolute right-2 top-1/2 -translate-y-1/2 px-4 py-1.5 rounded-lg bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium transition-colors">
|
| 53 |
+
Search
|
| 54 |
+
</button>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
{/* Content */}
|
| 60 |
+
<div className="max-w-4xl mx-auto px-6 py-8">
|
| 61 |
+
{/* Quick Actions */}
|
| 62 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
| 63 |
+
<button className="p-4 rounded-xl bg-white/[0.02] border border-white/10 hover:border-blue-500/30 transition-all text-left group">
|
| 64 |
+
<div className="flex items-center gap-3 mb-2">
|
| 65 |
+
<Sparkles className="w-5 h-5 text-purple-400" />
|
| 66 |
+
<span className="text-white font-medium">Ask Theologian</span>
|
| 67 |
+
</div>
|
| 68 |
+
<p className="text-sm text-neutral-500">Get deep theological insights</p>
|
| 69 |
+
</button>
|
| 70 |
+
<button className="p-4 rounded-xl bg-white/[0.02] border border-white/10 hover:border-blue-500/30 transition-all text-left group">
|
| 71 |
+
<div className="flex items-center gap-3 mb-2">
|
| 72 |
+
<Bookmark className="w-5 h-5 text-amber-400" />
|
| 73 |
+
<span className="text-white font-medium">My Bookmarks</span>
|
| 74 |
+
</div>
|
| 75 |
+
<p className="text-sm text-neutral-500">View saved verses</p>
|
| 76 |
+
</button>
|
| 77 |
+
<button className="p-4 rounded-xl bg-white/[0.02] border border-white/10 hover:border-blue-500/30 transition-all text-left group">
|
| 78 |
+
<div className="flex items-center gap-3 mb-2">
|
| 79 |
+
<History className="w-5 h-5 text-emerald-400" />
|
| 80 |
+
<span className="text-white font-medium">Study History</span>
|
| 81 |
+
</div>
|
| 82 |
+
<p className="text-sm text-neutral-500">Continue where you left off</p>
|
| 83 |
+
</button>
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
{/* Recent Searches */}
|
| 87 |
+
<div className="mb-8">
|
| 88 |
+
<h2 className="text-lg font-semibold text-white mb-4">Recent Searches</h2>
|
| 89 |
+
<div className="space-y-2">
|
| 90 |
+
{recentSearches.map((search, i) => (
|
| 91 |
+
<button
|
| 92 |
+
key={i}
|
| 93 |
+
className="w-full flex items-center gap-3 p-3 rounded-xl bg-white/[0.02] border border-white/5 hover:border-blue-500/30 transition-all text-left"
|
| 94 |
+
>
|
| 95 |
+
<History className="w-4 h-4 text-neutral-500" />
|
| 96 |
+
<span className="text-neutral-300 text-sm">{search}</span>
|
| 97 |
+
<ChevronRight className="w-4 h-4 text-neutral-500 ml-auto" />
|
| 98 |
+
</button>
|
| 99 |
+
))}
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
{/* Popular Books */}
|
| 104 |
+
<div>
|
| 105 |
+
<h2 className="text-lg font-semibold text-white mb-4">Browse by Book</h2>
|
| 106 |
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
| 107 |
+
{popularBooks.map((book) => (
|
| 108 |
+
<button
|
| 109 |
+
key={book.name}
|
| 110 |
+
className="p-4 rounded-xl bg-white/[0.02] border border-white/10 hover:border-blue-500/30 transition-all text-left group"
|
| 111 |
+
>
|
| 112 |
+
<div className="flex items-center justify-between mb-1">
|
| 113 |
+
<span className="text-white font-medium">{book.name}</span>
|
| 114 |
+
<ExternalLink className="w-3.5 h-3.5 text-neutral-500 opacity-0 group-hover:opacity-100 transition-opacity" />
|
| 115 |
+
</div>
|
| 116 |
+
<div className="flex items-center gap-2">
|
| 117 |
+
<span className="text-xs text-neutral-500">{book.chapters} chapters</span>
|
| 118 |
+
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-400">{book.category}</span>
|
| 119 |
+
</div>
|
| 120 |
+
</button>
|
| 121 |
+
))}
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
{/* AI Suggestion */}
|
| 126 |
+
<div className="mt-8 p-6 rounded-2xl bg-gradient-to-br from-blue-500/10 to-purple-500/10 border border-blue-500/20">
|
| 127 |
+
<div className="flex items-start gap-4">
|
| 128 |
+
<div className="w-12 h-12 rounded-xl bg-blue-500/20 border border-blue-500/30 flex items-center justify-center shrink-0">
|
| 129 |
+
<Sparkles className="w-6 h-6 text-blue-400" />
|
| 130 |
+
</div>
|
| 131 |
+
<div>
|
| 132 |
+
<h3 className="text-white font-semibold mb-1">Theologian Tip</h3>
|
| 133 |
+
<p className="text-neutral-400 text-sm leading-relaxed">
|
| 134 |
+
Try asking questions like "What does Paul mean by 'justified by faith' in Romans 3:28?"
|
| 135 |
+
or "Compare the creation accounts in Genesis 1 and 2" for deeper theological analysis.
|
| 136 |
+
</p>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
);
|
| 143 |
+
}
|
frontend/app/dashboard/explore/page.tsx
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import { Compass, BookOpen, Heart, Users, Sparkles, ChevronRight, Play, Clock, Star } from 'lucide-react';
|
| 5 |
+
|
| 6 |
+
const featuredTopics = [
|
| 7 |
+
{
|
| 8 |
+
title: 'Understanding Grace',
|
| 9 |
+
description: 'Explore the concept of grace throughout Scripture',
|
| 10 |
+
duration: '15 min read',
|
| 11 |
+
category: 'Theology',
|
| 12 |
+
color: 'purple',
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
title: 'Prayer 101',
|
| 16 |
+
description: 'Learn different forms of biblical prayer',
|
| 17 |
+
duration: '10 min read',
|
| 18 |
+
category: 'Spiritual Practice',
|
| 19 |
+
color: 'rose',
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
title: 'The Beatitudes',
|
| 23 |
+
description: 'Deep dive into Jesus\' Sermon on the Mount',
|
| 24 |
+
duration: '20 min read',
|
| 25 |
+
category: 'Bible Study',
|
| 26 |
+
color: 'blue',
|
| 27 |
+
},
|
| 28 |
+
];
|
| 29 |
+
|
| 30 |
+
const guidedStudies = [
|
| 31 |
+
{
|
| 32 |
+
title: '7 Days of Psalms',
|
| 33 |
+
description: 'A week-long journey through the Psalms',
|
| 34 |
+
days: 7,
|
| 35 |
+
progress: 3,
|
| 36 |
+
color: 'amber',
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
title: 'Foundations of Faith',
|
| 40 |
+
description: 'Core beliefs of Christianity explained',
|
| 41 |
+
days: 14,
|
| 42 |
+
progress: 0,
|
| 43 |
+
color: 'emerald',
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
title: 'The Life of Jesus',
|
| 47 |
+
description: 'Walk through the Gospels chronologically',
|
| 48 |
+
days: 30,
|
| 49 |
+
progress: 12,
|
| 50 |
+
color: 'blue',
|
| 51 |
+
},
|
| 52 |
+
];
|
| 53 |
+
|
| 54 |
+
const quickPrompts = [
|
| 55 |
+
'What does the Bible say about anxiety?',
|
| 56 |
+
'Explain the Trinity in simple terms',
|
| 57 |
+
'How can I strengthen my prayer life?',
|
| 58 |
+
'What are the fruits of the Spirit?',
|
| 59 |
+
'Help me understand forgiveness',
|
| 60 |
+
'What does it mean to love your neighbor?',
|
| 61 |
+
];
|
| 62 |
+
|
| 63 |
+
const colorClasses: Record<string, { bg: string; border: string; text: string; gradient: string }> = {
|
| 64 |
+
purple: { bg: 'bg-purple-500/10', border: 'border-purple-500/30', text: 'text-purple-400', gradient: 'from-purple-500 to-violet-500' },
|
| 65 |
+
blue: { bg: 'bg-blue-500/10', border: 'border-blue-500/30', text: 'text-blue-400', gradient: 'from-blue-500 to-cyan-500' },
|
| 66 |
+
amber: { bg: 'bg-amber-500/10', border: 'border-amber-500/30', text: 'text-amber-400', gradient: 'from-amber-500 to-orange-500' },
|
| 67 |
+
rose: { bg: 'bg-rose-500/10', border: 'border-rose-500/30', text: 'text-rose-400', gradient: 'from-rose-500 to-pink-500' },
|
| 68 |
+
emerald: { bg: 'bg-emerald-500/10', border: 'border-emerald-500/30', text: 'text-emerald-400', gradient: 'from-emerald-500 to-teal-500' },
|
| 69 |
+
cyan: { bg: 'bg-cyan-500/10', border: 'border-cyan-500/30', text: 'text-cyan-400', gradient: 'from-cyan-500 to-blue-500' },
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
export default function ExplorePage() {
|
| 73 |
+
return (
|
| 74 |
+
<div className="flex-1 overflow-y-auto">
|
| 75 |
+
{/* Header */}
|
| 76 |
+
<div className="sticky top-0 z-10 bg-[#0a0a0a]/90 backdrop-blur-xl border-b border-white/5">
|
| 77 |
+
<div className="max-w-5xl mx-auto px-6 py-4">
|
| 78 |
+
<div className="flex items-center gap-3">
|
| 79 |
+
<div className="w-10 h-10 rounded-xl bg-cyan-500/10 border border-cyan-500/30 flex items-center justify-center">
|
| 80 |
+
<Compass className="w-5 h-5 text-cyan-400" />
|
| 81 |
+
</div>
|
| 82 |
+
<div>
|
| 83 |
+
<h1 className="text-xl font-semibold text-white">Explore</h1>
|
| 84 |
+
<p className="text-sm text-neutral-500">Discover new spiritual topics and guided studies</p>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
<div className="max-w-5xl mx-auto px-6 py-8">
|
| 91 |
+
{/* Quick Prompts */}
|
| 92 |
+
<div className="mb-8">
|
| 93 |
+
<h2 className="text-lg font-semibold text-white mb-4">Quick Questions</h2>
|
| 94 |
+
<div className="flex flex-wrap gap-2">
|
| 95 |
+
{quickPrompts.map((prompt) => (
|
| 96 |
+
<button
|
| 97 |
+
key={prompt}
|
| 98 |
+
className="px-4 py-2 rounded-full bg-white/[0.02] border border-white/10 text-neutral-300 text-sm hover:border-cyan-500/30 hover:text-white transition-all"
|
| 99 |
+
>
|
| 100 |
+
{prompt}
|
| 101 |
+
</button>
|
| 102 |
+
))}
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
{/* Featured Topics */}
|
| 107 |
+
<div className="mb-8">
|
| 108 |
+
<div className="flex items-center justify-between mb-4">
|
| 109 |
+
<h2 className="text-lg font-semibold text-white">Featured Topics</h2>
|
| 110 |
+
<button className="text-sm text-cyan-400 hover:text-cyan-300 transition-colors">
|
| 111 |
+
View All
|
| 112 |
+
</button>
|
| 113 |
+
</div>
|
| 114 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
| 115 |
+
{featuredTopics.map((topic) => {
|
| 116 |
+
const colors = colorClasses[topic.color];
|
| 117 |
+
return (
|
| 118 |
+
<button
|
| 119 |
+
key={topic.title}
|
| 120 |
+
className={`p-5 rounded-xl ${colors.bg} border ${colors.border} hover:brightness-110 transition-all text-left group`}
|
| 121 |
+
>
|
| 122 |
+
<div className="flex items-center gap-2 mb-2">
|
| 123 |
+
<span className={`text-xs px-2 py-0.5 rounded-full ${colors.bg} ${colors.text} border ${colors.border}`}>
|
| 124 |
+
{topic.category}
|
| 125 |
+
</span>
|
| 126 |
+
<span className="text-xs text-neutral-500 flex items-center gap-1">
|
| 127 |
+
<Clock className="w-3 h-3" />
|
| 128 |
+
{topic.duration}
|
| 129 |
+
</span>
|
| 130 |
+
</div>
|
| 131 |
+
<h3 className="text-white font-semibold mb-1">{topic.title}</h3>
|
| 132 |
+
<p className="text-neutral-400 text-sm">{topic.description}</p>
|
| 133 |
+
<div className={`mt-3 flex items-center gap-1 ${colors.text} text-sm opacity-0 group-hover:opacity-100 transition-opacity`}>
|
| 134 |
+
<span>Start reading</span>
|
| 135 |
+
<ChevronRight className="w-4 h-4" />
|
| 136 |
+
</div>
|
| 137 |
+
</button>
|
| 138 |
+
);
|
| 139 |
+
})}
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
|
| 143 |
+
{/* Guided Studies */}
|
| 144 |
+
<div className="mb-8">
|
| 145 |
+
<div className="flex items-center justify-between mb-4">
|
| 146 |
+
<h2 className="text-lg font-semibold text-white">Guided Studies</h2>
|
| 147 |
+
<button className="text-sm text-cyan-400 hover:text-cyan-300 transition-colors">
|
| 148 |
+
Browse All
|
| 149 |
+
</button>
|
| 150 |
+
</div>
|
| 151 |
+
<div className="space-y-3">
|
| 152 |
+
{guidedStudies.map((study) => {
|
| 153 |
+
const colors = colorClasses[study.color];
|
| 154 |
+
const progressPercent = (study.progress / study.days) * 100;
|
| 155 |
+
return (
|
| 156 |
+
<div
|
| 157 |
+
key={study.title}
|
| 158 |
+
className="p-4 rounded-xl bg-white/[0.02] border border-white/10 hover:border-cyan-500/30 transition-all"
|
| 159 |
+
>
|
| 160 |
+
<div className="flex items-start justify-between mb-3">
|
| 161 |
+
<div>
|
| 162 |
+
<h3 className="text-white font-medium">{study.title}</h3>
|
| 163 |
+
<p className="text-neutral-500 text-sm">{study.description}</p>
|
| 164 |
+
</div>
|
| 165 |
+
<button className={`p-2 rounded-lg ${colors.bg} border ${colors.border} ${colors.text} hover:brightness-110 transition-all`}>
|
| 166 |
+
<Play className="w-4 h-4" />
|
| 167 |
+
</button>
|
| 168 |
+
</div>
|
| 169 |
+
<div className="flex items-center gap-3">
|
| 170 |
+
<div className="flex-1 h-2 bg-white/5 rounded-full overflow-hidden">
|
| 171 |
+
<div
|
| 172 |
+
className={`h-full rounded-full bg-gradient-to-r ${colors.gradient}`}
|
| 173 |
+
style={{ width: `${progressPercent}%` }}
|
| 174 |
+
/>
|
| 175 |
+
</div>
|
| 176 |
+
<span className="text-xs text-neutral-500">
|
| 177 |
+
{study.progress} / {study.days} days
|
| 178 |
+
</span>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
);
|
| 182 |
+
})}
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
{/* Categories */}
|
| 187 |
+
<div>
|
| 188 |
+
<h2 className="text-lg font-semibold text-white mb-4">Browse by Category</h2>
|
| 189 |
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
| 190 |
+
{[
|
| 191 |
+
{ icon: BookOpen, label: 'Bible Study', count: 24, color: 'blue' },
|
| 192 |
+
{ icon: Heart, label: 'Spiritual Growth', count: 18, color: 'rose' },
|
| 193 |
+
{ icon: Users, label: 'Relationships', count: 12, color: 'amber' },
|
| 194 |
+
{ icon: Sparkles, label: 'Theology', count: 15, color: 'purple' },
|
| 195 |
+
].map((category) => {
|
| 196 |
+
const colors = colorClasses[category.color];
|
| 197 |
+
return (
|
| 198 |
+
<button
|
| 199 |
+
key={category.label}
|
| 200 |
+
className={`p-4 rounded-xl ${colors.bg} border ${colors.border} hover:brightness-110 transition-all text-left`}
|
| 201 |
+
>
|
| 202 |
+
<category.icon className={`w-6 h-6 ${colors.text} mb-2`} />
|
| 203 |
+
<div className="text-white font-medium">{category.label}</div>
|
| 204 |
+
<div className="text-neutral-500 text-sm">{category.count} topics</div>
|
| 205 |
+
</button>
|
| 206 |
+
);
|
| 207 |
+
})}
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
|
| 211 |
+
{/* AI Suggestion */}
|
| 212 |
+
<div className="mt-8 p-6 rounded-2xl bg-gradient-to-br from-cyan-500/10 to-purple-500/10 border border-cyan-500/20">
|
| 213 |
+
<div className="flex items-start gap-4">
|
| 214 |
+
<div className="w-12 h-12 rounded-xl bg-cyan-500/20 border border-cyan-500/30 flex items-center justify-center shrink-0">
|
| 215 |
+
<Sparkles className="w-6 h-6 text-cyan-400" />
|
| 216 |
+
</div>
|
| 217 |
+
<div>
|
| 218 |
+
<h3 className="text-white font-semibold mb-1">Personalized for You</h3>
|
| 219 |
+
<p className="text-neutral-400 text-sm leading-relaxed">
|
| 220 |
+
Based on your recent conversations about faith and trust, you might enjoy exploring
|
| 221 |
+
"The Life of Abraham" study - it beautifully illustrates unwavering faith in God's promises.
|
| 222 |
+
</p>
|
| 223 |
+
<button className="mt-3 text-cyan-400 hover:text-cyan-300 text-sm font-medium transition-colors flex items-center gap-1">
|
| 224 |
+
Start Study
|
| 225 |
+
<ChevronRight className="w-4 h-4" />
|
| 226 |
+
</button>
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
</div>
|
| 232 |
+
);
|
| 233 |
+
}
|
frontend/app/dashboard/insights/page.tsx
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import { Brain, TrendingUp, Calendar, BookOpen, Heart, Sparkles, BarChart3, PieChart, ArrowUp, ArrowDown } from 'lucide-react';
|
| 5 |
+
|
| 6 |
+
const weeklyStats = [
|
| 7 |
+
{ label: 'Conversations', value: 12, change: +3, color: 'purple' },
|
| 8 |
+
{ label: 'Bible Searches', value: 28, change: +7, color: 'blue' },
|
| 9 |
+
{ label: 'Journal Entries', value: 5, change: +2, color: 'amber' },
|
| 10 |
+
{ label: 'Prayers', value: 8, change: -1, color: 'rose' },
|
| 11 |
+
];
|
| 12 |
+
|
| 13 |
+
const topTopics = [
|
| 14 |
+
{ topic: 'Faith & Trust', count: 15, percentage: 30 },
|
| 15 |
+
{ topic: 'Prayer & Meditation', count: 12, percentage: 24 },
|
| 16 |
+
{ topic: 'Scripture Study', count: 10, percentage: 20 },
|
| 17 |
+
{ topic: 'Life Guidance', count: 8, percentage: 16 },
|
| 18 |
+
{ topic: 'Emotional Support', count: 5, percentage: 10 },
|
| 19 |
+
];
|
| 20 |
+
|
| 21 |
+
const spiritualGrowth = [
|
| 22 |
+
{ month: 'Jul', score: 65 },
|
| 23 |
+
{ month: 'Aug', score: 70 },
|
| 24 |
+
{ month: 'Sep', score: 68 },
|
| 25 |
+
{ month: 'Oct', score: 75 },
|
| 26 |
+
{ month: 'Nov', score: 82 },
|
| 27 |
+
{ month: 'Dec', score: 88 },
|
| 28 |
+
];
|
| 29 |
+
|
| 30 |
+
const colorClasses: Record<string, { bg: string; border: string; text: string }> = {
|
| 31 |
+
purple: { bg: 'bg-purple-500/10', border: 'border-purple-500/30', text: 'text-purple-400' },
|
| 32 |
+
blue: { bg: 'bg-blue-500/10', border: 'border-blue-500/30', text: 'text-blue-400' },
|
| 33 |
+
amber: { bg: 'bg-amber-500/10', border: 'border-amber-500/30', text: 'text-amber-400' },
|
| 34 |
+
rose: { bg: 'bg-rose-500/10', border: 'border-rose-500/30', text: 'text-rose-400' },
|
| 35 |
+
emerald: { bg: 'bg-emerald-500/10', border: 'border-emerald-500/30', text: 'text-emerald-400' },
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
export default function InsightsPage() {
|
| 39 |
+
const [timeRange, setTimeRange] = useState<'week' | 'month' | 'year'>('week');
|
| 40 |
+
|
| 41 |
+
return (
|
| 42 |
+
<div className="flex-1 overflow-y-auto">
|
| 43 |
+
{/* Header */}
|
| 44 |
+
<div className="sticky top-0 z-10 bg-[#0a0a0a]/90 backdrop-blur-xl border-b border-white/5">
|
| 45 |
+
<div className="max-w-5xl mx-auto px-6 py-4">
|
| 46 |
+
<div className="flex items-center justify-between">
|
| 47 |
+
<div className="flex items-center gap-3">
|
| 48 |
+
<div className="w-10 h-10 rounded-xl bg-emerald-500/10 border border-emerald-500/30 flex items-center justify-center">
|
| 49 |
+
<Brain className="w-5 h-5 text-emerald-400" />
|
| 50 |
+
</div>
|
| 51 |
+
<div>
|
| 52 |
+
<h1 className="text-xl font-semibold text-white">Insights</h1>
|
| 53 |
+
<p className="text-sm text-neutral-500">Track your spiritual growth</p>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
<div className="flex gap-1 p-1 rounded-lg bg-white/5">
|
| 57 |
+
{(['week', 'month', 'year'] as const).map((range) => (
|
| 58 |
+
<button
|
| 59 |
+
key={range}
|
| 60 |
+
onClick={() => setTimeRange(range)}
|
| 61 |
+
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
| 62 |
+
timeRange === range
|
| 63 |
+
? 'bg-white/10 text-white'
|
| 64 |
+
: 'text-neutral-400 hover:text-white'
|
| 65 |
+
}`}
|
| 66 |
+
>
|
| 67 |
+
{range.charAt(0).toUpperCase() + range.slice(1)}
|
| 68 |
+
</button>
|
| 69 |
+
))}
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
<div className="max-w-5xl mx-auto px-6 py-8">
|
| 76 |
+
{/* Weekly Stats */}
|
| 77 |
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
| 78 |
+
{weeklyStats.map((stat) => {
|
| 79 |
+
const colors = colorClasses[stat.color];
|
| 80 |
+
return (
|
| 81 |
+
<div
|
| 82 |
+
key={stat.label}
|
| 83 |
+
className={`p-4 rounded-xl ${colors.bg} border ${colors.border}`}
|
| 84 |
+
>
|
| 85 |
+
<div className="flex items-center justify-between mb-2">
|
| 86 |
+
<span className="text-neutral-400 text-sm">{stat.label}</span>
|
| 87 |
+
<div className={`flex items-center gap-1 text-xs ${
|
| 88 |
+
stat.change >= 0 ? 'text-emerald-400' : 'text-rose-400'
|
| 89 |
+
}`}>
|
| 90 |
+
{stat.change >= 0 ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />}
|
| 91 |
+
{Math.abs(stat.change)}
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
<div className={`text-2xl font-bold ${colors.text}`}>{stat.value}</div>
|
| 95 |
+
</div>
|
| 96 |
+
);
|
| 97 |
+
})}
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
| 101 |
+
{/* Growth Chart */}
|
| 102 |
+
<div className="p-6 rounded-2xl bg-white/[0.02] border border-white/10">
|
| 103 |
+
<div className="flex items-center justify-between mb-6">
|
| 104 |
+
<h2 className="text-lg font-semibold text-white">Spiritual Growth</h2>
|
| 105 |
+
<BarChart3 className="w-5 h-5 text-neutral-500" />
|
| 106 |
+
</div>
|
| 107 |
+
<div className="flex items-end justify-between h-40 gap-2">
|
| 108 |
+
{spiritualGrowth.map((data) => (
|
| 109 |
+
<div key={data.month} className="flex-1 flex flex-col items-center gap-2">
|
| 110 |
+
<div
|
| 111 |
+
className="w-full bg-gradient-to-t from-emerald-500/50 to-emerald-400/30 rounded-t-lg transition-all"
|
| 112 |
+
style={{ height: `${data.score}%` }}
|
| 113 |
+
/>
|
| 114 |
+
<span className="text-xs text-neutral-500">{data.month}</span>
|
| 115 |
+
</div>
|
| 116 |
+
))}
|
| 117 |
+
</div>
|
| 118 |
+
<div className="mt-4 pt-4 border-t border-white/5 flex items-center justify-between">
|
| 119 |
+
<span className="text-sm text-neutral-400">Engagement Score</span>
|
| 120 |
+
<span className="text-emerald-400 font-semibold">88/100</span>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
|
| 124 |
+
{/* Top Topics */}
|
| 125 |
+
<div className="p-6 rounded-2xl bg-white/[0.02] border border-white/10">
|
| 126 |
+
<div className="flex items-center justify-between mb-6">
|
| 127 |
+
<h2 className="text-lg font-semibold text-white">Top Topics</h2>
|
| 128 |
+
<PieChart className="w-5 h-5 text-neutral-500" />
|
| 129 |
+
</div>
|
| 130 |
+
<div className="space-y-4">
|
| 131 |
+
{topTopics.map((topic, i) => (
|
| 132 |
+
<div key={topic.topic}>
|
| 133 |
+
<div className="flex items-center justify-between mb-1">
|
| 134 |
+
<span className="text-sm text-neutral-300">{topic.topic}</span>
|
| 135 |
+
<span className="text-sm text-neutral-500">{topic.percentage}%</span>
|
| 136 |
+
</div>
|
| 137 |
+
<div className="h-2 bg-white/5 rounded-full overflow-hidden">
|
| 138 |
+
<div
|
| 139 |
+
className={`h-full rounded-full ${
|
| 140 |
+
i === 0 ? 'bg-purple-500' :
|
| 141 |
+
i === 1 ? 'bg-rose-500' :
|
| 142 |
+
i === 2 ? 'bg-blue-500' :
|
| 143 |
+
i === 3 ? 'bg-amber-500' :
|
| 144 |
+
'bg-emerald-500'
|
| 145 |
+
}`}
|
| 146 |
+
style={{ width: `${topic.percentage}%` }}
|
| 147 |
+
/>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
))}
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
|
| 155 |
+
{/* AI Insights */}
|
| 156 |
+
<div className="p-6 rounded-2xl bg-gradient-to-br from-emerald-500/10 to-purple-500/10 border border-emerald-500/20">
|
| 157 |
+
<div className="flex items-start gap-4">
|
| 158 |
+
<div className="w-12 h-12 rounded-xl bg-emerald-500/20 border border-emerald-500/30 flex items-center justify-center shrink-0">
|
| 159 |
+
<Sparkles className="w-6 h-6 text-emerald-400" />
|
| 160 |
+
</div>
|
| 161 |
+
<div>
|
| 162 |
+
<h3 className="text-white font-semibold mb-2">ORA's Observation</h3>
|
| 163 |
+
<p className="text-neutral-400 text-sm leading-relaxed mb-4">
|
| 164 |
+
Your spiritual engagement has grown 35% this month! I've noticed you've been particularly
|
| 165 |
+
drawn to topics around faith and trust. Consider exploring the book of Hebrews,
|
| 166 |
+
which offers deep insights on faith in action.
|
| 167 |
+
</p>
|
| 168 |
+
<div className="flex flex-wrap gap-2">
|
| 169 |
+
<span className="px-3 py-1 rounded-full bg-emerald-500/10 text-emerald-400 text-xs">
|
| 170 |
+
+35% engagement
|
| 171 |
+
</span>
|
| 172 |
+
<span className="px-3 py-1 rounded-full bg-purple-500/10 text-purple-400 text-xs">
|
| 173 |
+
5-day streak
|
| 174 |
+
</span>
|
| 175 |
+
<span className="px-3 py-1 rounded-full bg-blue-500/10 text-blue-400 text-xs">
|
| 176 |
+
28 verses studied
|
| 177 |
+
</span>
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
{/* Goals */}
|
| 184 |
+
<div className="mt-8">
|
| 185 |
+
<h2 className="text-lg font-semibold text-white mb-4">Spiritual Goals</h2>
|
| 186 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
| 187 |
+
<div className="p-4 rounded-xl bg-white/[0.02] border border-white/10">
|
| 188 |
+
<div className="flex items-center gap-3 mb-3">
|
| 189 |
+
<BookOpen className="w-5 h-5 text-blue-400" />
|
| 190 |
+
<span className="text-white font-medium">Daily Reading</span>
|
| 191 |
+
</div>
|
| 192 |
+
<div className="h-2 bg-white/5 rounded-full overflow-hidden mb-2">
|
| 193 |
+
<div className="h-full bg-blue-500 rounded-full" style={{ width: '70%' }} />
|
| 194 |
+
</div>
|
| 195 |
+
<span className="text-xs text-neutral-500">5 of 7 days this week</span>
|
| 196 |
+
</div>
|
| 197 |
+
<div className="p-4 rounded-xl bg-white/[0.02] border border-white/10">
|
| 198 |
+
<div className="flex items-center gap-3 mb-3">
|
| 199 |
+
<Heart className="w-5 h-5 text-rose-400" />
|
| 200 |
+
<span className="text-white font-medium">Prayer Time</span>
|
| 201 |
+
</div>
|
| 202 |
+
<div className="h-2 bg-white/5 rounded-full overflow-hidden mb-2">
|
| 203 |
+
<div className="h-full bg-rose-500 rounded-full" style={{ width: '85%' }} />
|
| 204 |
+
</div>
|
| 205 |
+
<span className="text-xs text-neutral-500">6 of 7 days this week</span>
|
| 206 |
+
</div>
|
| 207 |
+
<div className="p-4 rounded-xl bg-white/[0.02] border border-white/10">
|
| 208 |
+
<div className="flex items-center gap-3 mb-3">
|
| 209 |
+
<Calendar className="w-5 h-5 text-amber-400" />
|
| 210 |
+
<span className="text-white font-medium">Journaling</span>
|
| 211 |
+
</div>
|
| 212 |
+
<div className="h-2 bg-white/5 rounded-full overflow-hidden mb-2">
|
| 213 |
+
<div className="h-full bg-amber-500 rounded-full" style={{ width: '40%' }} />
|
| 214 |
+
</div>
|
| 215 |
+
<span className="text-xs text-neutral-500">2 of 5 entries this week</span>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
</div>
|
| 221 |
+
);
|
| 222 |
+
}
|