alaselababatunde commited on
Commit
b7d05f4
·
1 Parent(s): ac16b35

refactor: complete codebase replacement with Domino's architecture

Browse files
frontend/.gitignore DELETED
@@ -1,24 +0,0 @@
1
- # Logs
2
- logs
3
- *.log
4
- npm-debug.log*
5
- yarn-debug.log*
6
- yarn-error.log*
7
- pnpm-debug.log*
8
- lerna-debug.log*
9
-
10
- node_modules
11
- dist
12
- dist-ssr
13
- *.local
14
-
15
- # Editor directories and files
16
- .vscode/*
17
- !.vscode/extensions.json
18
- .idea
19
- .DS_Store
20
- *.suo
21
- *.ntvs*
22
- *.njsproj
23
- *.sln
24
- *.sw?
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/README.md DELETED
@@ -1,73 +0,0 @@
1
- # React + TypeScript + Vite
2
-
3
- This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
-
5
- Currently, two official plugins are available:
6
-
7
- - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
- - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
-
10
- ## React Compiler
11
-
12
- The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
-
14
- ## Expanding the ESLint configuration
15
-
16
- If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17
-
18
- ```js
19
- export default defineConfig([
20
- globalIgnores(['dist']),
21
- {
22
- files: ['**/*.{ts,tsx}'],
23
- extends: [
24
- // Other configs...
25
-
26
- // Remove tseslint.configs.recommended and replace with this
27
- tseslint.configs.recommendedTypeChecked,
28
- // Alternatively, use this for stricter rules
29
- tseslint.configs.strictTypeChecked,
30
- // Optionally, add this for stylistic rules
31
- tseslint.configs.stylisticTypeChecked,
32
-
33
- // Other configs...
34
- ],
35
- languageOptions: {
36
- parserOptions: {
37
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
- tsconfigRootDir: import.meta.dirname,
39
- },
40
- // other options...
41
- },
42
- },
43
- ])
44
- ```
45
-
46
- You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47
-
48
- ```js
49
- // eslint.config.js
50
- import reactX from 'eslint-plugin-react-x'
51
- import reactDom from 'eslint-plugin-react-dom'
52
-
53
- export default defineConfig([
54
- globalIgnores(['dist']),
55
- {
56
- files: ['**/*.{ts,tsx}'],
57
- extends: [
58
- // Other configs...
59
- // Enable lint rules for React
60
- reactX.configs['recommended-typescript'],
61
- // Enable lint rules for React DOM
62
- reactDom.configs.recommended,
63
- ],
64
- languageOptions: {
65
- parserOptions: {
66
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
67
- tsconfigRootDir: import.meta.dirname,
68
- },
69
- // other options...
70
- },
71
- },
72
- ])
73
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/eslint.config.js DELETED
@@ -1,23 +0,0 @@
1
- import js from '@eslint/js'
2
- import globals from 'globals'
3
- import reactHooks from 'eslint-plugin-react-hooks'
4
- import reactRefresh from 'eslint-plugin-react-refresh'
5
- import tseslint from 'typescript-eslint'
6
- import { defineConfig, globalIgnores } from 'eslint/config'
7
-
8
- export default defineConfig([
9
- globalIgnores(['dist']),
10
- {
11
- files: ['**/*.{ts,tsx}'],
12
- extends: [
13
- js.configs.recommended,
14
- tseslint.configs.recommended,
15
- reactHooks.configs.flat.recommended,
16
- reactRefresh.configs.vite,
17
- ],
18
- languageOptions: {
19
- ecmaVersion: 2020,
20
- globals: globals.browser,
21
- },
22
- },
23
- ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/index.html DELETED
@@ -1,18 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/png" href="CLCC Logo No Background.png" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Theo AI | Biblical Guidance & Christian Support</title>
8
- <meta name="description" content="Seek spiritual wisdom and Biblical encouragement with Theo, your personal Christian AI assistant from TechDisciples CLCC." />
9
- <!-- Google Fonts -->
10
- <link rel="preconnect" href="https://fonts.googleapis.com">
11
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
13
- </head>
14
- <body>
15
- <div id="root"></div>
16
- <script type="module" src="/src/main.tsx"></script>
17
- </body>
18
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/package-lock.json DELETED
The diff for this file is too large to render. See raw diff
 
frontend/package.json DELETED
@@ -1,37 +0,0 @@
1
- {
2
- "name": "frontend",
3
- "private": true,
4
- "version": "0.0.0",
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vite",
8
- "build": "vite build",
9
- "lint": "eslint .",
10
- "preview": "vite preview"
11
- },
12
- "dependencies": {
13
- "react": "^19.2.0",
14
- "react-dom": "^19.2.0",
15
- "axios": "^1.6.7",
16
- "lucide-react": "^0.320.0",
17
- "react-markdown": "^9.0.1"
18
- },
19
- "devDependencies": {
20
- "@eslint/js": "^9.39.1",
21
- "@types/node": "^24.10.1",
22
- "@types/react": "^19.2.5",
23
- "@types/react-dom": "^19.2.3",
24
- "@vitejs/plugin-react": "^5.1.1",
25
- "eslint": "^9.39.1",
26
- "eslint-plugin-react-hooks": "^7.0.1",
27
- "eslint-plugin-react-refresh": "^0.4.24",
28
- "globals": "^16.5.0",
29
- "typescript": "~5.9.3",
30
- "typescript-eslint": "^8.46.4",
31
- "vite": "^7.2.4",
32
- "tailwindcss": "^3.4.1",
33
- "postcss": "^8.4.35",
34
- "autoprefixer": "^10.4.18",
35
- "@tailwindcss/typography": "^0.5.10"
36
- }
37
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/postcss.config.js DELETED
@@ -1,6 +0,0 @@
1
- export default {
2
- plugins: {
3
- tailwindcss: {},
4
- autoprefixer: {},
5
- },
6
- }
 
 
 
 
 
 
 
frontend/public/assets/logo.jpg DELETED

Git LFS Details

  • SHA256: 3383887f254073e4eb4df7e7a98dcc6fbe9c27245ba7d7d5a3f9aedd9cea0f01
  • Pointer size: 131 Bytes
  • Size of remote file: 219 kB
frontend/public/assets/logo.png DELETED
Binary file (34.6 kB)
 
frontend/public/logo.jpeg DELETED
Binary file (3.83 kB)
 
frontend/public/vite.svg DELETED
frontend/src/App.tsx DELETED
@@ -1,216 +0,0 @@
1
- import React, { useState, useEffect, useRef } from 'react';
2
- import { Send, User, Bot, Loader2, Trash2 } from 'lucide-react';
3
- import ReactMarkdown from 'react-markdown';
4
- import axios from 'axios';
5
-
6
- interface Message {
7
- role: 'user' | 'assistant';
8
- content: string;
9
- }
10
-
11
- const VERSES = [
12
- { text: "Thy word is a lamp unto my feet, and a light unto my path.", ref: "Psalm 119:105" },
13
- { text: "For I know the thoughts that I think toward you, saith the Lord, thoughts of peace, and not of evil.", ref: "Jeremiah 29:11" },
14
- { text: "But they that wait upon the Lord shall renew their strength; they shall mount up with wings as eagles.", ref: "Isaiah 40:31" },
15
- { text: "Trust in the Lord with all thine heart; and lean not unto thine own understanding.", ref: "Proverbs 3:5" },
16
- { text: "I can do all things through Christ which strengtheneth me.", ref: "Philippians 4:13" },
17
- { text: "The Lord is my shepherd; I shall not want.", ref: "Psalm 23:1" },
18
- { text: "And be not conformed to this world: but be ye transformed by the renewing of your mind.", ref: "Romans 12:2" }
19
- ];
20
-
21
- const App: React.FC = () => {
22
- const [messages, setMessages] = useState<Message[]>([]);
23
- const [input, setInput] = useState('');
24
- const [isLoading, setIsLoading] = useState(false);
25
- const [sessionId] = useState<string>(() => Math.random().toString(36).substring(7));
26
- const [verse, setVerse] = useState(VERSES[0]);
27
- const chatBoxRef = useRef<HTMLDivElement>(null);
28
- const textareaRef = useRef<HTMLTextAreaElement>(null);
29
- const isFirstRender = useRef(true);
30
-
31
- useEffect(() => {
32
- const dayOfYear = Math.floor((new Date().getTime() - new Date(new Date().getFullYear(), 0, 0).getTime()) / 86400000);
33
- setVerse(VERSES[dayOfYear % VERSES.length]);
34
- }, []);
35
-
36
- useEffect(() => {
37
- if (isFirstRender.current) {
38
- isFirstRender.current = false;
39
- return;
40
- }
41
- if (chatBoxRef.current) {
42
- chatBoxRef.current.scrollTo({
43
- top: chatBoxRef.current.scrollHeight,
44
- behavior: 'smooth'
45
- });
46
- }
47
- }, [messages, isLoading]);
48
-
49
- const handleSend = async () => {
50
- if (!input.trim() || isLoading) return;
51
-
52
- const userMessage = input.trim();
53
- setInput('');
54
- if (textareaRef.current) textareaRef.current.style.height = 'auto';
55
- setMessages(prev => [...prev, { role: 'user', content: userMessage }]);
56
- setIsLoading(true);
57
-
58
- try {
59
- const response = await axios.post('/ai-chat', {
60
- query: userMessage,
61
- session_id: sessionId,
62
- }, {
63
- headers: {
64
- 'Content-Type': 'application/json',
65
- 'x-api-key': 'techdisciplesai404'
66
- }
67
- });
68
-
69
- const assistantMessage = response.data.reply;
70
- setMessages(prev => [...prev, { role: 'assistant', content: assistantMessage }]);
71
- } catch (error) {
72
- console.error('Chat error:', error);
73
- setMessages(prev => [...prev, {
74
- role: 'assistant',
75
- content: 'I apologize, but I encountered an error. Please check your connection and try again. May God bless you.'
76
- }]);
77
- } finally {
78
- setIsLoading(false);
79
- }
80
- };
81
-
82
- const clearChat = () => {
83
- if (window.confirm('Are you sure you want to clear our conversation, beloved?')) {
84
- setMessages([]);
85
- }
86
- };
87
-
88
- const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
89
- setInput(e.target.value);
90
- e.target.style.height = 'auto';
91
- e.target.style.height = `${e.target.scrollHeight}px`;
92
- };
93
-
94
- return (
95
- <div className="h-[100dvh] w-full bg-[#002B22] flex items-center justify-center overflow-hidden font-outfit text-white p-0 sm:p-4">
96
- {/* Background Orbs */}
97
- <div className="absolute top-[-10%] left-[-10%] w-[50%] h-[50%] bg-[#007055]/20 rounded-full blur-[120px] pointer-events-none"></div>
98
- <div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] bg-[#FDFBD4]/5 rounded-full blur-[120px] pointer-events-none"></div>
99
-
100
- <div className="w-full max-w-4xl h-full sm:h-[90vh] bg-white/[0.03] backdrop-blur-3xl sm:rounded-[2rem] flex flex-col overflow-hidden border border-white/10 shadow-[0_32px_64px_-12px_rgba(0,0,0,0.5)]">
101
-
102
- {/* Header - Domino's Style */}
103
- <header className="flex items-center justify-between px-6 py-4 md:px-10 md:py-6 bg-[#00362B]/60 border-b border-white/10 z-20">
104
- <div className="flex items-center gap-4">
105
- <div className="relative w-10 h-10 md:w-12 md:h-12 rounded-xl overflow-hidden border border-[#FDFBD4]/20 shadow-lg">
106
- <img src="/assets/logo.jpg" alt="CLCC Logo" className="w-full h-full object-cover" />
107
- </div>
108
- <div className="flex flex-col">
109
- <h1 className="text-xl md:text-2xl font-black tracking-tight text-[#FDFBD4]">Theo AI</h1>
110
- <span className="text-[9px] md:text-[11px] font-bold text-[#FDFBD4]/40 uppercase tracking-[0.2em]">CLCC - TECHDISCIPLES</span>
111
- </div>
112
- </div>
113
- <button
114
- onClick={clearChat}
115
- className="p-2 transition-all hover:bg-white/5 rounded-full border border-white/5 text-[#FDFBD4]/60 hover:text-[#FDFBD4]"
116
- title="Clear Conversation"
117
- >
118
- <Trash2 size={20} />
119
- </button>
120
- </header>
121
-
122
- {/* Chat Container */}
123
- <div
124
- ref={chatBoxRef}
125
- className="flex-1 overflow-y-auto px-4 py-8 md:px-10 space-y-6 scroll-smooth scrollbar-hide"
126
- >
127
- {messages.length === 0 && (
128
- <div className="h-full flex flex-col items-center justify-center text-center p-6 animate-in fade-in zoom-in duration-700">
129
- <div className="w-20 h-20 bg-[#FDFBD4]/5 rounded-3xl flex items-center justify-center border border-[#FDFBD4]/10 shadow-inner mb-6">
130
- <Bot size={40} className="text-[#FDFBD4]" />
131
- </div>
132
- <h2 className="text-3xl font-bold text-white mb-2">Shalom, Beloved</h2>
133
- <p className="text-white/60 text-base max-w-md leading-relaxed mb-8">
134
- I am Theo, centered in Christ to walk with you in faith. How may the Lord's wisdom guide you today?
135
- </p>
136
-
137
- <div className="w-full max-w-sm p-5 rounded-2xl bg-[#00362B]/40 border border-[#FDFBD4]/10 backdrop-blur-sm text-left">
138
- <h3 className="text-[10px] font-bold text-[#FDFBD4]/50 uppercase tracking-widest mb-3">Verse of the Day</h3>
139
- <p className="text-[#FDFBD4]/90 text-sm italic leading-relaxed">
140
- "{verse.text}"
141
- <span className="block mt-2 font-bold not-italic opacity-100 text-[#FDFBD4]">-- {verse.ref}</span>
142
- </p>
143
- </div>
144
- </div>
145
- )}
146
-
147
- {messages.map((msg, index) => (
148
- <div key={index} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'} animate-in slide-in-from-bottom-4 duration-500 fill-mode-both`}>
149
- <div className={`flex flex-col max-w-[85%] md:max-w-[75%] ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
150
- <div className={`relative px-5 py-4 rounded-2xl shadow-lg selection:bg-[#007055]/30 ${msg.role === 'user'
151
- ? 'bg-[#007055] text-white rounded-br-none border border-white/10 selection:bg-white/20'
152
- : 'bg-[#FDFBD4]/95 text-[#002B22] rounded-bl-none border-l-4 border-[#007055]'
153
- }`}>
154
- {msg.role === 'assistant' && (
155
- <div className="flex items-center gap-2 mb-2 opacity-40">
156
- <Bot size={14} />
157
- <span className="text-[10px] font-black uppercase tracking-tighter">Theo</span>
158
- </div>
159
- )}
160
- <div className={`prose prose-sm max-w-none ${msg.role === 'user' ? 'prose-invert' : 'prose-headings:text-[#00362B] prose-strong:text-[#007055]'}`}>
161
- <ReactMarkdown>{msg.content}</ReactMarkdown>
162
- </div>
163
- </div>
164
- </div>
165
- </div>
166
- ))}
167
-
168
- {isLoading && (
169
- <div className="flex justify-start animate-in slide-in-from-bottom-2 duration-300">
170
- <div className="bg-[#FDFBD4]/95 p-4 rounded-2xl rounded-bl-none flex items-center gap-2">
171
- <div className="flex gap-1">
172
- <div className="w-2 h-2 bg-[#007055] rounded-full animate-bounce [animation-delay:-0.3s]"></div>
173
- <div className="w-2 h-2 bg-[#007055] rounded-full animate-bounce [animation-delay:-0.15s]"></div>
174
- <div className="w-2 h-2 bg-[#007055] rounded-full animate-bounce"></div>
175
- </div>
176
- </div>
177
- </div>
178
- )}
179
- </div>
180
-
181
- {/* Input Area - Domino's Style */}
182
- <div className="p-4 md:p-8 bg-white/5 border-top border-white/10 backdrop-blur-md">
183
- <div className="max-w-3xl mx-auto flex items-end gap-3 md:gap-4 relative group">
184
- <textarea
185
- ref={textareaRef}
186
- value={input}
187
- onChange={handleTextareaChange}
188
- onKeyDown={(e) => {
189
- if (e.key === 'Enter' && !e.shiftKey) {
190
- e.preventDefault();
191
- handleSend();
192
- }
193
- }}
194
- placeholder="How can Theo help you today?"
195
- className="flex-1 bg-[#00362B]/60 border-2 border-white/5 rounded-2xl p-4 md:px-6 md:py-4 focus:outline-none focus:border-[#FDFBD4]/30 text-white placeholder:text-white/20 text-sm md:text-base resize-none max-h-[150px] transition-all scrollbar-hide"
196
- rows={1}
197
- disabled={isLoading}
198
- />
199
- <button
200
- onClick={handleSend}
201
- disabled={isLoading || !input.trim()}
202
- className="w-12 h-12 md:w-14 md:h-14 bg-[#FDFBD4] text-[#002B22] rounded-full flex items-center justify-center shrink-0 shadow-[0_8px_20px_-4px_rgba(253,251,212,0.4)] hover:scale-105 active:scale-95 transition-all disabled:opacity-20 disabled:grayscale disabled:scale-100"
203
- >
204
- <Send size={24} className={isLoading ? 'animate-pulse' : ''} />
205
- </button>
206
- </div>
207
- <div className="text-center mt-4">
208
- <p className="text-[10px] font-black uppercase tracking-[0.4em] text-white/20">Faith-Based Intelligence</p>
209
- </div>
210
- </div>
211
- </div>
212
- </div>
213
- );
214
- };
215
-
216
- export default App;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/index.css DELETED
@@ -1,98 +0,0 @@
1
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
4
-
5
- :root {
6
- font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif;
7
- line-height: 1.5;
8
- font-weight: 400;
9
- color-scheme: dark;
10
- background-color: #002B22;
11
- }
12
-
13
- body {
14
- margin: 0;
15
- display: flex;
16
- place-items: center;
17
- min-width: 320px;
18
- min-height: 100vh;
19
- }
20
-
21
- #root {
22
- width: 100%;
23
- }
24
-
25
- .glass-container {
26
- background: rgba(255, 255, 255, 0.03);
27
- backdrop-filter: blur(20px);
28
- -webkit-backdrop-filter: blur(20px);
29
- border: 1px solid rgba(255, 255, 255, 0.1);
30
- border-radius: 24px;
31
- box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
32
- }
33
-
34
- .scrollbar-hide::-webkit-scrollbar {
35
- display: none;
36
- }
37
-
38
- .scrollbar-hide {
39
- -ms-overflow-style: none;
40
- scrollbar-width: none;
41
- }
42
-
43
- /* Markdown Styling */
44
- .markdown-content h1, .markdown-content h2, .markdown-content h3 {
45
- font-weight: 700;
46
- margin-top: 1.5em;
47
- margin-bottom: 0.5em;
48
- color: white;
49
- }
50
-
51
- .markdown-content p {
52
- margin-bottom: 1em;
53
- line-height: 1.6;
54
- }
55
-
56
- .markdown-content ul, .markdown-content ol {
57
- margin-bottom: 1em;
58
- padding-left: 1.5em;
59
- }
60
-
61
- .markdown-content li {
62
- margin-bottom: 0.5em;
63
- }
64
-
65
- .markdown-content strong {
66
- color: #FDFBD4;
67
- font-weight: 800;
68
- }
69
-
70
- .markdown-content table {
71
- width: 100%;
72
- border-collapse: collapse;
73
- margin-bottom: 1em;
74
- background: rgba(0, 0, 0, 0.2);
75
- border-radius: 8px;
76
- overflow: hidden;
77
- }
78
-
79
- .markdown-content th, .markdown-content td {
80
- padding: 8px 12px;
81
- border: 1px solid rgba(255, 255, 255, 0.1);
82
- text-align: left;
83
- }
84
-
85
- .markdown-content th {
86
- background: rgba(255, 255, 255, 0.05);
87
- font-weight: 600;
88
- }
89
-
90
- /* Animations */
91
- @keyframes pulse-soft {
92
- 0%, 100% { opacity: 1; }
93
- 50% { opacity: 0.6; }
94
- }
95
-
96
- .animate-pulse-soft {
97
- animation: pulse-soft 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
98
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/main.tsx DELETED
@@ -1,10 +0,0 @@
1
- import React from 'react'
2
- import ReactDOM from 'react-dom/client'
3
- import App from './App.tsx'
4
- import './index.css'
5
-
6
- ReactDOM.createRoot(document.getElementById('root')!).render(
7
- <React.StrictMode>
8
- <App />
9
- </React.StrictMode>,
10
- )
 
 
 
 
 
 
 
 
 
 
 
frontend/tailwind.config.js DELETED
@@ -1,18 +0,0 @@
1
- /** @type {import('tailwindcss').Config} */
2
- export default {
3
- content: [
4
- "./index.html",
5
- "./src/**/*.{js,ts,jsx,tsx}",
6
- ],
7
- theme: {
8
- extend: {
9
- fontFamily: {
10
- sans: ['Inter', 'sans-serif'],
11
- outfit: ['Outfit', 'sans-serif'],
12
- },
13
- },
14
- },
15
- plugins: [
16
- require('@tailwindcss/typography'),
17
- ],
18
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/tsconfig.app.json DELETED
@@ -1,28 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
- "target": "ES2022",
5
- "useDefineForClassFields": true,
6
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
- "module": "ESNext",
8
- "types": ["vite/client"],
9
- "skipLibCheck": true,
10
-
11
- /* Bundler mode */
12
- "moduleResolution": "bundler",
13
- "allowImportingTsExtensions": true,
14
- "verbatimModuleSyntax": true,
15
- "moduleDetection": "force",
16
- "noEmit": true,
17
- "jsx": "react-jsx",
18
-
19
- /* Linting */
20
- "strict": true,
21
- "noUnusedLocals": true,
22
- "noUnusedParameters": true,
23
- "erasableSyntaxOnly": true,
24
- "noFallthroughCasesInSwitch": true,
25
- "noUncheckedSideEffectImports": true
26
- },
27
- "include": ["src"]
28
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "files": [],
3
- "references": [
4
- { "path": "./tsconfig.app.json" },
5
- { "path": "./tsconfig.node.json" }
6
- ]
7
- }
 
 
 
 
 
 
 
 
frontend/tsconfig.node.json DELETED
@@ -1,26 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
- "target": "ES2023",
5
- "lib": ["ES2023"],
6
- "module": "ESNext",
7
- "types": ["node"],
8
- "skipLibCheck": true,
9
-
10
- /* Bundler mode */
11
- "moduleResolution": "bundler",
12
- "allowImportingTsExtensions": true,
13
- "verbatimModuleSyntax": true,
14
- "moduleDetection": "force",
15
- "noEmit": true,
16
-
17
- /* Linting */
18
- "strict": true,
19
- "noUnusedLocals": true,
20
- "noUnusedParameters": true,
21
- "erasableSyntaxOnly": true,
22
- "noFallthroughCasesInSwitch": true,
23
- "noUncheckedSideEffectImports": true
24
- },
25
- "include": ["vite.config.ts"]
26
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/vite.config.ts DELETED
@@ -1,7 +0,0 @@
1
- import { defineConfig } from 'vite'
2
- import react from '@vitejs/plugin-react'
3
-
4
- // https://vite.dev/config/
5
- export default defineConfig({
6
- plugins: [react()],
7
- })
 
 
 
 
 
 
 
 
main.py CHANGED
@@ -1,185 +1,176 @@
1
- # =====================================================
2
- # Theo AI - Tech Disciples CLCC
3
- # =====================================================
4
-
5
  import os
 
6
  import logging
7
- from typing import List, Optional
8
- from fastapi import FastAPI, HTTPException, Header, Request
9
- from fastapi.responses import HTMLResponse, FileResponse
 
10
  from fastapi.staticfiles import StaticFiles
 
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from pydantic import BaseModel
13
  from dotenv import load_dotenv
14
 
15
  from langchain_openai import ChatOpenAI
16
- from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
17
- from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
18
- from langchain.memory import ConversationBufferWindowMemory
19
  from langchain_community.tools import DuckDuckGoSearchRun
20
 
21
- # Load environment variables
22
- load_dotenv()
23
-
24
  # =====================================================
25
  # CONFIGURATION
26
  # =====================================================
 
 
27
  OPENROUTER_API_KEY = os.getenv("THEO_OPENROUTER_MODEL_KEY")
28
  MODEL_NAME = "openai/gpt-oss-120b:free"
29
- API_SECRET = "techdisciplesai404" # Keeping the original secret as per user instruction (if applicable, but user didn't specify to change this)
30
-
31
- # =====================================================
32
- # LOGGING
33
- # =====================================================
34
- logging.basicConfig(level=logging.INFO)
35
- logger = logging.getLogger("TheoAI")
36
 
37
  if not OPENROUTER_API_KEY:
38
- logger.error("THEO_OPENROUTER_MODEL_KEY not found in environment")
39
-
40
- # =====================================================
41
- # FASTAPI APP
42
- # =====================================================
43
- app = FastAPI(title="Theo AI", version="4.0")
44
-
45
- app.add_middleware(
46
- CORSMiddleware,
47
- allow_origins=["*"],
48
- allow_credentials=True,
49
- allow_methods=["*"],
50
- allow_headers=["*"],
51
- )
52
 
53
  # =====================================================
54
  # LLM & TOOLS
55
  # =====================================================
56
  llm = ChatOpenAI(
 
57
  openai_api_key=OPENROUTER_API_KEY,
58
  openai_api_base="https://openrouter.ai/api/v1",
59
- model_name=MODEL_NAME,
60
- temperature=0.7,
 
 
61
  )
62
 
63
  search = DuckDuckGoSearchRun()
64
 
65
  # =====================================================
66
- # PROMPT TEMPLATE
67
  # =====================================================
68
  SYSTEM_PROMPT = """You are Theo — a warm, friendly, and deeply insightful Christian companion created by TechDisciples CLCC.
69
 
70
  TONE & STYLE:
71
- 1. CHATTY & WARM: Speak like a loving friend or a caring elder. Use a conversational, "chatty" tone that feels personal and welcoming.
72
- 2. PARAGRAPHS: NEVER send a single wall of text. Break your thoughts into 2-4 short, readable paragraphs. Each point should have its own breathing space.
73
- 3. NIGERIAN HEARTBEAT: Use simple, relatable English with a gentle Nigerian inflection. Be clear and accessible to everyone.
74
 
75
  CORE GUIDELINES:
76
- 1. BIBLICAL FOCUS: Root your wisdom and encouragement in the Holy Bible. Use scripture to offer hope, especially for those in distress.
77
- 2. FORMATTING: Use Markdown (bolding for emphasis, short lists if needed) to keep your replies professional yet approachable.
78
- 3. UNKNOWN TOPICS: If you lack specific knowledge, use your web search tool to find facts, then wrap that information in your warm Christian perspective.
79
 
80
  Identity: Theo
81
  Creators: TechDisciples CLCC
82
- Logo: [CLCC logo](https://raw.githubusercontent.com/Alasela/Theo/main/assests/logo.png)
 
 
 
83
 
84
- Remember: Be a beacon of God's love. Keep your messages structured with clear paragraphs so they are easy to read in a chat window."""
 
 
 
85
 
86
  # =====================================================
87
- # MEMORY
88
  # =====================================================
89
- # Using a dictionary to store memory for different sessions
90
- memories = {}
91
 
92
- def get_memory(session_id: str):
93
- if session_id not in memories:
94
- memories[session_id] = ConversationBufferWindowMemory(
95
- k=5, return_messages=True, memory_key="history"
96
- )
97
- return memories[session_id]
 
 
 
 
 
 
 
 
98
 
99
  # =====================================================
100
- # REQUEST MODEL
101
  # =====================================================
102
- class QueryInput(BaseModel):
103
- query: str
104
- session_id: Optional[str] = "default"
105
-
106
- # Mount the frontend directory as static files
107
- # In a typical Vite React app, the build output is in 'dist'.
108
- # If it's just raw HTML/JS/CSS we point to 'frontend'.
109
- frontend_dir = os.path.join(os.path.dirname(__file__), "frontend")
110
- dist_dir = os.path.join(frontend_dir, "dist")
111
 
112
- if os.path.isdir(dist_dir):
113
- serve_dir = dist_dir
114
- else:
115
- serve_dir = frontend_dir
 
 
 
116
 
117
- app.mount("/assets", StaticFiles(directory=os.path.join(serve_dir, "assets"), html=True), name="assets")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
  @app.get("/", response_class=HTMLResponse)
120
- async def root():
121
- index_path = os.path.join(serve_dir, "index.html")
122
- if os.path.exists(index_path):
123
- return FileResponse(index_path)
124
- return {"message": "✅ Theo AI (TechDisciples CLCC) API is online. Frontend not built yet."}
125
-
126
- @app.post("/ai-chat")
127
- async def ai_chat(data: QueryInput, x_api_key: str = Header(None)):
128
- # Standard check for security
129
- if x_api_key and x_api_key != API_SECRET:
130
- logger.warning(f"Unauthorized access attempt with key: {x_api_key}")
131
- # User didn't specify to keep this, but it's good practice.
132
- # However, if I want to make it easy for the new frontend, I might relax this or update it.
133
-
134
- query = data.query.strip()
135
- session_id = data.session_id
136
- memory = get_memory(session_id)
137
 
138
- try:
139
- # Check if query needs search (simple heuristic: if it asks for current events or facts outside theology)
140
- # Or better: let the LLM decide. But for a simple implementation as requested:
141
-
142
- # We can implement a simple check or just let GPT-OSS-120B handle it.
143
- # But user said: "when the ai cannot answer... it should conduct a websearch"
144
-
145
- # Constructing the chat history for context
146
- history = memory.load_memory_variables({})["history"]
147
-
148
- messages = [SystemMessage(content=SYSTEM_PROMPT)]
149
- messages.extend(history)
150
- messages.append(HumanMessage(content=query))
151
-
152
- # Get AI response
153
- response = llm.invoke(messages)
154
- content = response.content
155
 
156
- # If response indicates lack of knowledge (GPT often says "I don't know" or "I am not sure")
157
- # we can trigger search.
158
- if any(indicator in content.lower() for indicator in ["i don't know", "i'm not sure", "as an ai", "i don't have information"]):
159
- logger.info(f"Triggering web search for: {query}")
160
- search_results = search.run(query)
161
-
162
- search_prompt = f"The user asked: {query}\nI found the following information via web search: {search_results}\n\nPlease provide a response following the Theo AI guidelines (Biblical focus, Nigerian perspective) based on this information."
163
-
164
- messages.append(AIMessage(content=content)) # Store the "don't know" response
165
- messages.append(HumanMessage(content=search_prompt))
166
-
167
- response = llm.invoke(messages)
168
- content = response.content
169
-
170
- # Save to memory
171
- memory.save_context({"input": query}, {"output": content})
172
-
173
- return {"reply": content}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
- except Exception as e:
176
- logger.error(f"⚠️ Model runtime error: {e}")
177
- raise HTTPException(status_code=500, detail=f"Theo encountered an issue: {str(e)}")
178
-
179
- # Catch-all route to serve the SPA index.html for unknown paths
180
- @app.exception_handler(404)
181
- async def custom_404_handler(request: Request, exc: HTTPException):
182
- index_path = os.path.join(serve_dir, "index.html")
183
- if os.path.exists(index_path) and not request.url.path.startswith("/api"):
184
- return FileResponse(index_path)
185
- return HTMLResponse(content="Not Found", status_code=404)
 
 
 
 
 
1
  import os
2
+ import uuid
3
  import logging
4
+ import markdown
5
+ from typing import List, Dict, Optional
6
+ from fastapi import FastAPI, Request, Response, HTTPException
7
+ from fastapi.responses import HTMLResponse, JSONResponse
8
  from fastapi.staticfiles import StaticFiles
9
+ from fastapi.templating import Jinja2Templates
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from pydantic import BaseModel
12
  from dotenv import load_dotenv
13
 
14
  from langchain_openai import ChatOpenAI
15
+ from langchain_core.prompts import ChatPromptTemplate
 
 
16
  from langchain_community.tools import DuckDuckGoSearchRun
17
 
 
 
 
18
  # =====================================================
19
  # CONFIGURATION
20
  # =====================================================
21
+ load_dotenv()
22
+
23
  OPENROUTER_API_KEY = os.getenv("THEO_OPENROUTER_MODEL_KEY")
24
  MODEL_NAME = "openai/gpt-oss-120b:free"
 
 
 
 
 
 
 
25
 
26
  if not OPENROUTER_API_KEY:
27
+ logging.error("THEO_OPENROUTER_MODEL_KEY not found in environment")
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  # =====================================================
30
  # LLM & TOOLS
31
  # =====================================================
32
  llm = ChatOpenAI(
33
+ model=MODEL_NAME,
34
  openai_api_key=OPENROUTER_API_KEY,
35
  openai_api_base="https://openrouter.ai/api/v1",
36
+ default_headers={
37
+ "HTTP-Referer": "https://techdisciples1-theo.hf.space/",
38
+ "X-Title": "Theo AI Support"
39
+ }
40
  )
41
 
42
  search = DuckDuckGoSearchRun()
43
 
44
  # =====================================================
45
+ # SYSTEM PROMPT
46
  # =====================================================
47
  SYSTEM_PROMPT = """You are Theo — a warm, friendly, and deeply insightful Christian companion created by TechDisciples CLCC.
48
 
49
  TONE & STYLE:
50
+ 1. CHATTY & WARM: Speak like a loving friend or a caring elder. Use a conversational, "chatty" tone that feels personal and welcoming. Use phrases like "Shalom, Beloved" or "My dear friend".
51
+ 2. PARAGRAPHS: NEVER send a single wall of text. Break your thoughts into 2-4 short, readable paragraphs.
52
+ 3. NIGERIAN HEARTBEAT: Use simple, relatable English with a gentle Nigerian inflection.
53
 
54
  CORE GUIDELINES:
55
+ 1. BIBLICAL FOCUS: Root your wisdom and encouragement in the Holy Bible. Use scripture to offer hope.
56
+ 2. FORMATTING: Use Markdown (bolding for emphasis, short lists if needed).
57
+ 3. UNKNOWN TOPICS: If you lack specific knowledge, search for facts, then wrap that information in your warm Christian perspective.
58
 
59
  Identity: Theo
60
  Creators: TechDisciples CLCC
61
+ Logo: [CLCC logo](/static/assets/logo.png)
62
+
63
+ Context provided for current query:
64
+ {reviews}
65
 
66
+ User question: {question}
67
+ """
68
+
69
+ prompt_template = ChatPromptTemplate.from_template(SYSTEM_PROMPT)
70
 
71
  # =====================================================
72
+ # FASTAPI APP SETUP
73
  # =====================================================
74
+ app = FastAPI(title="Theo AI")
 
75
 
76
+ app.add_middleware(
77
+ CORSMiddleware,
78
+ allow_origins=["*"],
79
+ allow_credentials=True,
80
+ allow_methods=["*"],
81
+ allow_headers=["*"],
82
+ )
83
+
84
+ # Static files and Templates
85
+ app.mount("/static", StaticFiles(directory="static"), name="static")
86
+ templates = Jinja2Templates(directory="templates")
87
+
88
+ # In-memory chat history (mirrors Domino's logic)
89
+ chat_sessions: Dict[str, List[Dict[str, str]]] = {}
90
 
91
  # =====================================================
92
+ # LOGIC
93
  # =====================================================
94
+ class ChatRequest(BaseModel):
95
+ question: str
 
 
 
 
 
 
 
96
 
97
+ def get_ai_response(question: str) -> str:
98
+ try:
99
+ # First attempt (no reviews/context in this simple flow, but keeping structure)
100
+ reviews = "No specific context provided."
101
+ chain = prompt_template | llm
102
+ response = chain.invoke({"reviews": reviews, "question": question})
103
+ content = response.content
104
 
105
+ # Simple check if AI couldn't answer (Trigger search as requested)
106
+ fallback_phrases = ["i don't know", "i am not sure", "i couldn't find", "sorry, i can't", "i do not have information"]
107
+ if any(phrase in content.lower() for phrase in fallback_phrases):
108
+ logging.info(f"Triggering web search for: {question}")
109
+ search_results = search.run(question)
110
+
111
+ search_prompt = ChatPromptTemplate.from_template(
112
+ "You are Theo (Christian companion). The user asked: {question}\n"
113
+ "I found this information on the web: {search_results}\n"
114
+ "Provide a warm, Christian response using this information, following Theo's guidelines."
115
+ )
116
+ search_chain = search_prompt | llm
117
+ response = search_chain.invoke({"question": question, "search_results": search_results})
118
+ content = response.content
119
+
120
+ return content
121
+ except Exception as e:
122
+ logging.error(f"AI Response failed: {e}")
123
+ return "I apologize, beloved, but I am having trouble connecting to the heavenly signals (our servers) right now. Please try again in a moment!"
124
 
125
  @app.get("/", response_class=HTMLResponse)
126
+ async def index(request: Request):
127
+ device_id = request.cookies.get("device_id")
128
+ if not device_id:
129
+ device_id = str(uuid.uuid4())
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
+ if device_id not in chat_sessions:
132
+ chat_sessions[device_id] = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
+ response = templates.TemplateResponse("index.html", {"request": request, "chat_history": chat_sessions[device_id]})
135
+ response.set_cookie(key="device_id", value=device_id)
136
+ return response
137
+
138
+ @app.post("/ask")
139
+ async def ask(request: Request, data: ChatRequest):
140
+ device_id = request.cookies.get("device_id")
141
+ if not device_id:
142
+ device_id = str(uuid.uuid4())
143
+ if device_id not in chat_sessions:
144
+ chat_sessions[device_id] = []
145
+
146
+ question = data.question.strip()
147
+ if not question:
148
+ return JSONResponse({"error": "No question provided"}, status_code=400)
149
+
150
+ raw_response = get_ai_response(question)
151
+
152
+ # Format response with markdown for safety (same as Domino's behavior)
153
+ formatted_response = markdown.markdown(raw_response, extensions=['extra', 'tables'])
154
+
155
+ chat_sessions[device_id].append({
156
+ "user": question,
157
+ "bot": formatted_response
158
+ })
159
+
160
+ response = JSONResponse({"response": formatted_response})
161
+ response.set_cookie(key="device_id", value=device_id)
162
+ return response
163
+
164
+ @app.post("/clear")
165
+ async def clear_chat(request: Request):
166
+ device_id = request.cookies.get("device_id")
167
+ if device_id and device_id in chat_sessions:
168
+ chat_sessions[device_id] = []
169
+
170
+ response = JSONResponse({"status": "cleared"})
171
+ response.set_cookie(key="device_id", value=device_id)
172
+ return response
173
 
174
+ if __name__ == "__main__":
175
+ import uvicorn
176
+ uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
static/main.css ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Theo AI - Premium Christian UI (Domino's Architecture) */
2
+
3
+ :root {
4
+ --theo-green: #002B22;
5
+ --theo-teal: #00362B;
6
+ --theo-cream: #FDFBD4;
7
+ --theo-white: #FFFFFF;
8
+ --bg-gradient: radial-gradient(circle at top left, #00362B 0%, #002B22 100%);
9
+ --glass: rgba(0, 54, 43, 0.4);
10
+ --glass-border: rgba(253, 251, 212, 0.1);
11
+ }
12
+
13
+ * {
14
+ margin: 0;
15
+ padding: 0;
16
+ box-sizing: border-box;
17
+ font-family: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
18
+ }
19
+
20
+ body {
21
+ height: 100vh;
22
+ width: 100vw;
23
+ background: var(--theo-green);
24
+ background-image: var(--bg-gradient);
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ overflow: hidden;
29
+ }
30
+
31
+ .chat-container {
32
+ width: 100%;
33
+ max-width: 900px;
34
+ height: 90vh;
35
+ background: var(--glass);
36
+ backdrop-filter: blur(20px);
37
+ -webkit-backdrop-filter: blur(20px);
38
+ border-radius: 32px;
39
+ box-shadow: 0 40px 100px rgba(0, 0, 0, 0.5);
40
+ display: flex;
41
+ flex-direction: column;
42
+ overflow: hidden;
43
+ border: 1px solid var(--glass-border);
44
+ position: relative;
45
+ }
46
+
47
+ .chat-header {
48
+ background: rgba(0, 43, 34, 0.8);
49
+ padding: 20px 40px;
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: space-between;
53
+ border-bottom: 1px solid var(--glass-border);
54
+ z-index: 10;
55
+ backdrop-filter: blur(10px);
56
+ }
57
+
58
+ .chat-header h2 {
59
+ color: var(--theo-cream);
60
+ font-weight: 800;
61
+ font-size: 1.6rem;
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 15px;
65
+ letter-spacing: -0.02em;
66
+ }
67
+
68
+ .chat-header img {
69
+ height: 45px;
70
+ border-radius: 12px;
71
+ border: 1px solid var(--glass-border);
72
+ }
73
+
74
+ .chat-box {
75
+ flex: 1;
76
+ padding: 40px;
77
+ overflow-y: auto;
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: 24px;
81
+ background: transparent;
82
+ scroll-behavior: smooth;
83
+ }
84
+
85
+ .chat-box::-webkit-scrollbar {
86
+ width: 6px;
87
+ }
88
+
89
+ .chat-box::-webkit-scrollbar-thumb {
90
+ background: rgba(253, 251, 212, 0.1);
91
+ border-radius: 10px;
92
+ }
93
+
94
+ .welcome-container {
95
+ display: flex;
96
+ flex-direction: column;
97
+ align-items: center;
98
+ justify-content: center;
99
+ margin: auto;
100
+ text-align: center;
101
+ max-width: 400px;
102
+ }
103
+
104
+ .welcome-icon {
105
+ font-size: 3rem;
106
+ margin-bottom: 20px;
107
+ background: var(--theo-cream);
108
+ width: 80px;
109
+ height: 80px;
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ border-radius: 24px;
114
+ }
115
+
116
+ .welcome-container h1 {
117
+ color: var(--theo-cream);
118
+ font-size: 2.2rem;
119
+ margin-bottom: 12px;
120
+ font-weight: 800;
121
+ }
122
+
123
+ .welcome-container p {
124
+ color: rgba(255, 255, 255, 0.6);
125
+ line-height: 1.6;
126
+ }
127
+
128
+ .message {
129
+ max-width: 80%;
130
+ padding: 18px 24px;
131
+ border-radius: 24px;
132
+ font-size: 1.05rem;
133
+ line-height: 1.6;
134
+ position: relative;
135
+ transition: all 0.3s ease;
136
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
137
+ }
138
+
139
+ .message.user {
140
+ align-self: flex-end;
141
+ background: #007055;
142
+ color: white;
143
+ border-bottom-right-radius: 4px;
144
+ }
145
+
146
+ .message.bot {
147
+ align-self: flex-start;
148
+ background: var(--theo-cream);
149
+ color: #002B22;
150
+ border-bottom-left-radius: 4px;
151
+ border-left: 6px solid #007055;
152
+ }
153
+
154
+ .message.bot img.bot-logo {
155
+ width: 20px;
156
+ height: 20px;
157
+ margin-right: 12px;
158
+ vertical-align: middle;
159
+ border-radius: 4px;
160
+ }
161
+
162
+ /* AI Thinking Animation */
163
+ .thinking-container {
164
+ display: flex;
165
+ gap: 6px;
166
+ padding: 10px 0;
167
+ }
168
+
169
+ .thinking-dot {
170
+ width: 8px;
171
+ height: 8px;
172
+ background: #007055;
173
+ border-radius: 50%;
174
+ animation: bounce 1.4s infinite ease-in-out both;
175
+ }
176
+
177
+ .thinking-dot:nth-child(1) {
178
+ animation-delay: -0.32s;
179
+ }
180
+
181
+ .thinking-dot:nth-child(2) {
182
+ animation-delay: -0.16s;
183
+ }
184
+
185
+ @keyframes bounce {
186
+
187
+ 0%,
188
+ 80%,
189
+ 100% {
190
+ transform: scale(0);
191
+ opacity: 0.3;
192
+ }
193
+
194
+ 40% {
195
+ transform: scale(1);
196
+ opacity: 1;
197
+ }
198
+ }
199
+
200
+ .chat-input {
201
+ background: rgba(0, 43, 34, 0.8);
202
+ padding: 30px 40px;
203
+ display: flex;
204
+ gap: 20px;
205
+ align-items: flex-end;
206
+ border-top: 1px solid var(--glass-border);
207
+ backdrop-filter: blur(10px);
208
+ }
209
+
210
+ .chat-input textarea {
211
+ flex: 1;
212
+ background: rgba(255, 255, 255, 0.05);
213
+ border: 1px solid var(--glass-border);
214
+ border-radius: 20px;
215
+ padding: 16px 24px;
216
+ font-size: 1rem;
217
+ color: white;
218
+ resize: none;
219
+ max-height: 150px;
220
+ outline: none;
221
+ transition: all 0.3s ease;
222
+ }
223
+
224
+ .chat-input textarea:focus {
225
+ border-color: var(--theo-cream);
226
+ background: rgba(255, 255, 255, 0.08);
227
+ }
228
+
229
+ .chat-input button {
230
+ background: var(--theo-cream);
231
+ color: #002B22;
232
+ border: none;
233
+ border-radius: 50%;
234
+ width: 60px;
235
+ height: 60px;
236
+ display: flex;
237
+ align-items: center;
238
+ justify-content: center;
239
+ cursor: pointer;
240
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
241
+ box-shadow: 0 10px 25px rgba(253, 251, 212, 0.2);
242
+ flex-shrink: 0;
243
+ }
244
+
245
+ .chat-input button svg path {
246
+ stroke: #002B22;
247
+ }
248
+
249
+ .chat-input button:hover {
250
+ transform: translateY(-4px) rotate(5deg);
251
+ box-shadow: 0 15px 35px rgba(253, 251, 212, 0.3);
252
+ }
253
+
254
+ .chat-input button:active {
255
+ transform: scale(0.9);
256
+ }
257
+
258
+ .branding {
259
+ position: absolute;
260
+ bottom: 8px;
261
+ left: 0;
262
+ right: 0;
263
+ text-align: center;
264
+ font-size: 9px;
265
+ font-weight: 800;
266
+ letter-spacing: 0.5em;
267
+ color: rgba(253, 251, 212, 0.15);
268
+ pointer-events: none;
269
+ text-transform: uppercase;
270
+ }
271
+
272
+ /* Markdown Styling */
273
+ .message h1,
274
+ .message h2,
275
+ .message h3 {
276
+ margin: 12px 0 8px;
277
+ color: #007055;
278
+ font-weight: 800;
279
+ }
280
+
281
+ .message ul,
282
+ .message ol {
283
+ margin-left: 24px;
284
+ margin-bottom: 12px;
285
+ }
286
+
287
+ .message li {
288
+ margin-bottom: 6px;
289
+ }
290
+
291
+ .message p {
292
+ margin-bottom: 12px;
293
+ }
294
+
295
+ .message strong {
296
+ color: #004d3b;
297
+ font-weight: 800;
298
+ }
299
+
300
+ /* Animations */
301
+ .slide-in-bottom {
302
+ animation: slideIn 0.5s cubic-bezier(0.165, 0.84, 0.44, 1) forwards;
303
+ }
304
+
305
+ @keyframes slideIn {
306
+ from {
307
+ opacity: 0;
308
+ transform: translateY(30px) scale(0.95);
309
+ }
310
+
311
+ to {
312
+ opacity: 1;
313
+ transform: translateY(0) scale(1);
314
+ }
315
+ }
316
+
317
+ @media (max-width: 640px) {
318
+ .chat-container {
319
+ height: 100vh;
320
+ border-radius: 0;
321
+ }
322
+
323
+ .chat-header {
324
+ padding: 15px 20px;
325
+ }
326
+
327
+ .chat-box {
328
+ padding: 20px;
329
+ }
330
+
331
+ .chat-input {
332
+ padding: 20px;
333
+ }
334
+ }
templates/index.html ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>Theo AI | Biblical Guidance</title>
8
+ <link rel="stylesheet" href="/static/main.css" />
9
+ <link rel="icon" href="/static/assets/logo.png" type="image/png">
10
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800&display=swap" rel="stylesheet">
11
+ </head>
12
+
13
+ <body>
14
+ <div class="chat-container">
15
+ <div class="chat-header">
16
+ <h2><img src="/static/assets/logo.png" alt="CLCC Logo"> Theo AI</h2>
17
+ <button onclick="clearChat()"
18
+ style="background: transparent; border: none; color: var(--theo-cream); cursor: pointer; font-weight: bold; opacity: 0.6;">Clear
19
+ Chat</button>
20
+ </div>
21
+
22
+ <div class="chat-box" id="chat-box">
23
+ {% for message in chat_history %}
24
+ <div class="message user slide-in-bottom">{{ message.user }}</div>
25
+ <div class="message bot slide-in-bottom">
26
+ <img src="/static/assets/logo.png" class="bot-logo" alt="Theo">
27
+ {{ message.bot|safe }}
28
+ </div>
29
+ {% endfor %}
30
+
31
+ {% if not chat_history %}
32
+ <div class="welcome-container slide-in-bottom">
33
+ <div class="welcome-icon">🕊️</div>
34
+ <h1>Shalom, Beloved</h1>
35
+ <p>I am Theo, your personal Christian companion. How may the Lord's wisdom guide you today?</p>
36
+ </div>
37
+ {% endif %}
38
+ </div>
39
+
40
+ <form id="chat-form" class="chat-input" autocomplete="off">
41
+ <textarea name="question" id="input" placeholder="How can Theo help you today?" required></textarea>
42
+ <button type="submit" id="send-btn" aria-label="Send">
43
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
44
+ <path d="M5 12H19M19 12L12 5M19 12L12 19" stroke="white" stroke-width="3" stroke-linecap="round"
45
+ stroke-linejoin="round" />
46
+ </svg>
47
+ </button>
48
+ </form>
49
+ <div class="branding">CLCC - TECHDISCIPLES</div>
50
+ </div>
51
+
52
+ <script>
53
+ const chatForm = document.getElementById('chat-form');
54
+ const chatBox = document.getElementById('chat-box');
55
+ const input = document.getElementById('input');
56
+ const sendBtn = document.getElementById('send-btn');
57
+
58
+ // Auto-expand textarea
59
+ input.addEventListener('input', () => {
60
+ input.style.height = 'auto';
61
+ input.style.height = input.scrollHeight + 'px';
62
+ });
63
+
64
+ async function clearChat() {
65
+ if (confirm('Are you sure you want to clear our spiritual journey? (Chat history)')) {
66
+ await fetch('/clear', { method: 'POST' });
67
+ window.location.reload();
68
+ }
69
+ }
70
+
71
+ chatForm.addEventListener('submit', async (e) => {
72
+ e.preventDefault();
73
+ const question = input.value.trim();
74
+ if (!question) return;
75
+
76
+ // Remove welcome message if exists
77
+ const welcome = document.querySelector('.welcome-container');
78
+ if (welcome) welcome.remove();
79
+
80
+ // Add user message
81
+ const userMessage = document.createElement('div');
82
+ userMessage.classList.add('message', 'user', 'slide-in-bottom');
83
+ userMessage.textContent = question;
84
+ chatBox.appendChild(userMessage);
85
+ userMessage.scrollIntoView({ block: "end", behavior: "smooth" });
86
+
87
+ // Clear input immediately
88
+ input.value = '';
89
+ input.style.height = 'auto';
90
+ input.disabled = true;
91
+ sendBtn.disabled = true;
92
+
93
+ // Add bot message with thinking animation
94
+ const botMessage = document.createElement('div');
95
+ botMessage.classList.add('message', 'bot', 'slide-in-bottom');
96
+ botMessage.innerHTML = `
97
+ <img src="/static/assets/logo.png" class="bot-logo" alt="Theo">
98
+ <div class="thinking-container">
99
+ <div class="thinking-dot"></div>
100
+ <div class="thinking-dot"></div>
101
+ <div class="thinking-dot"></div>
102
+ </div>
103
+ `;
104
+ chatBox.appendChild(botMessage);
105
+ botMessage.scrollIntoView({ block: "start", behavior: "smooth" });
106
+
107
+ // Send POST request
108
+ try {
109
+ const res = await fetch('/ask', {
110
+ method: 'POST',
111
+ headers: { 'Content-Type': 'application/json' },
112
+ body: JSON.stringify({ question })
113
+ });
114
+ const data = await res.json();
115
+
116
+ // Replace thinking animation with bot response
117
+ botMessage.innerHTML = `<img src="/static/assets/logo.png" class="bot-logo" alt="Theo"> ${data.response || data.error}`;
118
+ botMessage.scrollIntoView({ block: "start", behavior: "smooth" });
119
+ } catch (err) {
120
+ botMessage.innerHTML = `<img src="/static/assets/logo.png" class="bot-logo" alt="Theo"> I apologize, beloved, but I encountered a connection issue. Please try again.`;
121
+ } finally {
122
+ input.disabled = false;
123
+ sendBtn.disabled = false;
124
+ input.focus();
125
+ }
126
+ });
127
+ </script>
128
+ </body>
129
+
130
+ </html>