SoniAI commited on
Commit
5884015
·
verified ·
1 Parent(s): 8e42a5d

Upload folder using huggingface_hub

Browse files
Files changed (13) hide show
  1. .gitignore +8 -0
  2. .python-version +1 -0
  3. README.md +2 -2
  4. app.py +2 -332
  5. logic/__init__.py +2 -0
  6. logic/profile.py +158 -0
  7. logic/tools.py +83 -0
  8. pyproject.toml +13 -0
  9. requirements.txt +5 -6
  10. ui/__init__.py +2 -0
  11. ui/layout.py +89 -0
  12. ui/styles.css +163 -0
  13. uv.lock +0 -0
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ .env
3
+ .env.*
4
+ __pycache__/
5
+ *.pyc
6
+ .vscode/
7
+ .idea/
8
+ *.log
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.12
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
- title: Chat_with_Evison
3
  app_file: app.py
4
  sdk: gradio
5
- sdk_version: 5.49.1
6
  ---
 
1
  ---
2
+ title: chat_with_evison
3
  app_file: app.py
4
  sdk: gradio
5
+ sdk_version: 6.1.0
6
  ---
app.py CHANGED
@@ -1,336 +1,6 @@
1
- from dotenv import load_dotenv
2
- from openai import OpenAI
3
- import json
4
- import os
5
- import requests
6
- from pypdf import PdfReader
7
- import gradio as gr
8
-
9
- load_dotenv(override=True)
10
-
11
-
12
- # ---------- Tools (Pushover logging) ----------
13
-
14
- def push(text):
15
- try:
16
- requests.post(
17
- "https://api.pushover.net/1/messages.json",
18
- data={
19
- "token": os.getenv("PUSHOVER_TOKEN"),
20
- "user": os.getenv("PUSHOVER_USER"),
21
- "message": text,
22
- },
23
- timeout=5,
24
- )
25
- except Exception as e:
26
- # Fail silently in the UI – this is just for logging
27
- print(f"Pushover error: {e}", flush=True)
28
-
29
-
30
- def record_user_details(email, name="Name not provided", notes="not provided"):
31
- push(f"Recording {name} with email {email} and notes {notes}")
32
- return {"recorded": "ok"}
33
-
34
-
35
- def record_unknown_question(question):
36
- push(f"User made an interesting question that I could not answer: {question}")
37
- return {"recorded": "ok"}
38
-
39
-
40
- record_user_details_json = {
41
- "name": "record_user_details",
42
- "description": "Use this tool to record that a user is interested in being in touch and provided an email address",
43
- "parameters": {
44
- "type": "object",
45
- "properties": {
46
- "email": {
47
- "type": "string",
48
- "description": "The email address of this user"
49
- },
50
- "name": {
51
- "type": "string",
52
- "description": "The user's name, if they provided it"
53
- },
54
- "notes": {
55
- "type": "string",
56
- "description": "Any additional information about the conversation that's worth recording to give context"
57
- }
58
- },
59
- "required": ["email"],
60
- "additionalProperties": False
61
- }
62
- }
63
-
64
- record_unknown_question_json = {
65
- "name": "record_unknown_question",
66
- "description": "Always use this tool to record any question that couldn't be answered as you didn't know the answer",
67
- "parameters": {
68
- "type": "object",
69
- "properties": {
70
- "question": {
71
- "type": "string",
72
- "description": "The question that couldn't be answered"
73
- },
74
- },
75
- "required": ["question"],
76
- "additionalProperties": False
77
- }
78
- }
79
-
80
- tools = [
81
- {"type": "function", "function": record_user_details_json},
82
- {"type": "function", "function": record_unknown_question_json},
83
- ]
84
-
85
-
86
- # ---------- Me class (your digital twin) ----------
87
-
88
- class Me:
89
-
90
- def __init__(self):
91
- self.openai = OpenAI()
92
- self.name = "Evison Ndoni"
93
-
94
- # Load LinkedIn-ish PDF text
95
- self.linkedin = ""
96
- try:
97
- reader = PdfReader("me/evison.pdf")
98
- for page in reader.pages:
99
- text = page.extract_text()
100
- if text:
101
- self.linkedin += text + "\n"
102
- except Exception as e:
103
- print(f"Error reading PDF: {e}", flush=True)
104
-
105
- # Load summary
106
- try:
107
- with open("me/summary.txt", "r", encoding="utf-8") as f:
108
- self.summary = f.read()
109
- except Exception as e:
110
- print(f"Error reading summary.txt: {e}", flush=True)
111
- self.summary = ""
112
-
113
- # Extra hard-coded context about you (for a richer persona)
114
- self.extra_context = """
115
- - 26-year-old software developer based in Tirana, Albania.
116
- - Strong frontend background: React, Next.js, TypeScript, HTML, CSS, SCSS, Tailwind.
117
- - Mobile experience with Flutter, especially for polished, fluid UI/UX.
118
- - Building portfolio projects like SeatReserve (seat booking SaaS) with modern full-stack tools.
119
- - Currently learning Agentic AI and aiming to grow into a strong AI / Agentic AI engineer.
120
- - Values: disciplined growth, responsibility, integrity, and putting God first.
121
- - Interested in roles that blend software engineering with AI, problem solving, and product thinking.
122
- """.strip()
123
-
124
- def handle_tool_call(self, tool_calls):
125
- results = []
126
- for tool_call in tool_calls:
127
- tool_name = tool_call.function.name
128
- arguments = json.loads(tool_call.function.arguments)
129
- print(f"Tool called: {tool_name}", flush=True)
130
- tool = globals().get(tool_name)
131
- result = tool(**arguments) if tool else {}
132
- results.append({
133
- "role": "tool",
134
- "content": json.dumps(result),
135
- "tool_call_id": tool_call.id
136
- })
137
- return results
138
-
139
- def system_prompt(self):
140
- system_prompt = f"""
141
- You are acting as {self.name}. You are answering questions on {self.name}'s personal website / career page,
142
- particularly questions related to {self.name}'s career, background, skills, experience, and aspirations.
143
-
144
- Your responsibilities:
145
- - Represent {self.name} as faithfully as possible.
146
- - Use the provided summary, LinkedIn-style profile information, and extra context below.
147
- - Be professional, clear, honest, and engaging – as if talking to a potential employer, client, or collaborator.
148
- - You can talk about his technical skills (React, Next.js, TypeScript, Flutter, Tailwind, etc.), portfolio projects,
149
- and his interest in Agentic AI engineering and AI-powered products.
150
- - You can also briefly mention his values (faith, discipline, integrity) when it’s appropriate for the conversation.
151
-
152
- Tools:
153
- - If you don't know the answer to any question, use the `record_unknown_question` tool to log the question.
154
- - If the user is a potential lead or seems interested in working with {self.name}, gently encourage them to share
155
- their email and log it using the `record_user_details` tool with a short note about the context.
156
-
157
- Tone:
158
- - Friendly, confident, thoughtful.
159
- - Don't oversell; be realistic but optimistic.
160
- - Keep answers concise but helpful, unless the user clearly wants depth.
161
-
162
- Below is your context. Use it actively:
163
-
164
- ## Summary (CV-style overview):
165
- {self.summary}
166
-
167
- ## LinkedIn-like Profile (from PDF):
168
- {self.linkedin}
169
-
170
- ## Extra Context:
171
- {self.extra_context}
172
-
173
- With this context, please chat with the user, always staying in character as {self.name}.
174
- """.strip()
175
- return system_prompt
176
-
177
- def chat(self, message, history):
178
- """
179
- message: latest user message (str)
180
- history: list[dict] with {'role': 'user'|'assistant', 'content': str}
181
- (we use type="messages" in Gradio to match this format)
182
- """
183
- messages = [{"role": "system", "content": self.system_prompt()}] + history + [
184
- {"role": "user", "content": message}
185
- ]
186
-
187
- done = False
188
- response_message = None
189
-
190
- while not done:
191
- response = self.openai.chat.completions.create(
192
- model="gpt-4o-mini",
193
- messages=messages,
194
- tools=tools
195
- )
196
- choice = response.choices[0]
197
- finish_reason = choice.finish_reason
198
-
199
- if finish_reason == "tool_calls":
200
- message_with_tools = choice.message
201
- tool_calls = message_with_tools.tool_calls
202
- results = self.handle_tool_call(tool_calls)
203
- messages.append(message_with_tools)
204
- messages.extend(results)
205
- else:
206
- done = True
207
- response_message = choice.message
208
-
209
- return response_message.content if response_message else "Sorry, something went wrong."
210
-
211
-
212
- # ---------- Gradio UI ----------
213
-
214
- me = Me()
215
-
216
- # Custom chat handler for Gradio
217
- def respond(user_message, history):
218
- """
219
- user_message: str from the textbox
220
- history: list of messages (role/content) used by the chatbot (type='messages')
221
- """
222
- if history is None:
223
- history = []
224
-
225
- assistant_reply = me.chat(user_message, history)
226
-
227
- # Update history with the last user + assistant turn
228
- new_history = history + [
229
- {"role": "user", "content": user_message},
230
- {"role": "assistant", "content": assistant_reply},
231
- ]
232
- return "", new_history, new_history # (cleared textbox, chatbot value, state)
233
-
234
-
235
- def clear_chat():
236
- return [], [] # chatbot, state
237
 
 
238
 
239
  if __name__ == "__main__":
240
- theme = gr.themes.Soft() # no radius_scale -> compatible with more Gradio versions
241
-
242
- with gr.Blocks(theme=theme, css="""
243
- #app-container {
244
- max-width: 1100px;
245
- margin: 0 auto;
246
- }
247
- .chat-title {
248
- text-align: center;
249
- font-size: 1.8rem;
250
- font-weight: 700;
251
- margin-bottom: 0.4rem;
252
- }
253
- .chat-subtitle {
254
- text-align: center;
255
- font-size: 0.95rem;
256
- opacity: 0.85;
257
- margin-bottom: 1.2rem;
258
- }
259
- """) as demo:
260
- with gr.Column(elem_id="app-container"):
261
- gr.Markdown(
262
- f"""
263
- <div class="chat-title">Career Conversation with <span style="color:#b91c1c;">{me.name}</span></div>
264
- <div class="chat-subtitle">
265
- Ask me about my experience, projects, skills, or how I can help your team.
266
- </div>
267
- """,
268
- elem_id="title-block"
269
- )
270
-
271
- with gr.Row():
272
- # Left: Chat area
273
- with gr.Column(scale=3):
274
- chatbot = gr.Chatbot(
275
- label="Chat with Evison",
276
- type="messages",
277
- height=480,
278
- show_copy_button=True,
279
- )
280
-
281
- user_input = gr.Textbox(
282
- label="Your message",
283
- placeholder="Ask me about my experience, tech stack, or projects...",
284
- lines=3,
285
- )
286
-
287
- with gr.Row():
288
- send_btn = gr.Button("Send", variant="primary")
289
- clear_btn = gr.Button("Clear chat")
290
-
291
- # Right: About panel
292
- with gr.Column(scale=2):
293
- gr.Markdown(
294
- f"""
295
- ### About {me.name}
296
-
297
- - Frontend & mobile engineer (React, Next.js, TypeScript, Flutter, Tailwind).
298
- - 3+ years of hands-on experience building real-world products.
299
- - Passionate about clean, modern UI/UX and maintainable codebases.
300
- - Currently learning **Agentic AI** and building AI-powered solutions.
301
-
302
- ### What you can ask
303
-
304
- - Details about my recent projects (e.g. SeatReserve).
305
- - How I approach architecture, testing, performance, or collaboration.
306
- - How I see myself contributing to your team or product.
307
- - My availability, preferred work style, and career goals.
308
-
309
- If you're interested in working together, feel free to ask for my email
310
- or share yours so we can continue the conversation.
311
- """.strip()
312
- )
313
-
314
- # State to hold full message history (in OpenAI format)
315
- history_state = gr.State([])
316
-
317
- # Wire up events
318
- send_btn.click(
319
- fn=respond,
320
- inputs=[user_input, history_state],
321
- outputs=[user_input, chatbot, history_state],
322
- )
323
-
324
- user_input.submit(
325
- fn=respond,
326
- inputs=[user_input, history_state],
327
- outputs=[user_input, chatbot, history_state],
328
- )
329
-
330
- clear_btn.click(
331
- fn=clear_chat,
332
- inputs=None,
333
- outputs=[chatbot, history_state],
334
- )
335
-
336
  demo.launch()
 
1
+ from ui.layout import build_interface
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ demo = build_interface()
4
 
5
  if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  demo.launch()
logic/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Logic package
2
+
logic/profile.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from dotenv import load_dotenv
5
+ from openai import OpenAI
6
+ from pypdf import PdfReader
7
+
8
+ from logic.tools import FUNCTION_MAP, TOOLS
9
+
10
+ load_dotenv(override=True)
11
+
12
+ BASE_DIR = Path(__file__).resolve().parent.parent
13
+
14
+
15
+ class Me:
16
+ def __init__(self):
17
+ self.openai = OpenAI()
18
+ self.name = "Evison Ndoni"
19
+
20
+
21
+ pdf_path = BASE_DIR / "me" / "evison.pdf"
22
+ self.linkedin = ""
23
+ try:
24
+ if pdf_path.exists():
25
+ reader = PdfReader(str(pdf_path))
26
+ for page in reader.pages:
27
+ text = page.extract_text()
28
+ if text:
29
+ self.linkedin += text
30
+ else:
31
+ self.linkedin = "(LinkedIn PDF not found on server.)"
32
+ except Exception as e:
33
+
34
+ self.linkedin = f"(Error reading LinkedIn PDF: {e})"
35
+
36
+
37
+ summary_path = BASE_DIR / "me" / "summary.txt"
38
+ try:
39
+ with open(summary_path, "r", encoding="utf-8") as f:
40
+ self.summary = f.read()
41
+ except Exception as e:
42
+ self.summary = f"(Summary file not found or unreadable: {e})"
43
+
44
+
45
+ self.extra_context = """
46
+ - 26-year-old software engineer from Albania.
47
+ - 3+ years experience with React, Next.js, TypeScript, Tailwind CSS and Flutter.
48
+ - Currently learning Agentic AI and aiming for AI Engineer roles.
49
+ - Values clean, fluid UI/UX and likes to build useful products and SaaS.
50
+ - Tries to stay grounded, responsible, and future-oriented while putting God first.
51
+ """.strip()
52
+
53
+ def handle_tool_call(self, tool_calls):
54
+ results = []
55
+ for tool_call in tool_calls:
56
+ tool_name = tool_call.function.name
57
+ arguments = json.loads(tool_call.function.arguments)
58
+ print(f"Tool called: {tool_name}", flush=True)
59
+ tool = FUNCTION_MAP.get(tool_name)
60
+ result = tool(**arguments) if tool else {}
61
+ results.append(
62
+ {
63
+ "role": "tool",
64
+ "content": json.dumps(result),
65
+ "tool_call_id": tool_call.id,
66
+ }
67
+ )
68
+ return results
69
+
70
+ def system_prompt(self):
71
+ system_prompt = (
72
+ f"You are acting as {self.name}. You are answering questions on "
73
+ f"{self.name}'s personal website, particularly questions related to his "
74
+ f"career, background, skills, experience, and values.\n\n"
75
+ f"Your responsibility is to represent {self.name} as faithfully as possible. "
76
+ f"Be professional, warm, confident, and grounded, as if talking to a "
77
+ f"potential client, hiring manager, or collaborator.\n\n"
78
+ f"{self.name} is a follower of the Orthodox Christian faith and tries to put "
79
+ f"God first while being ambitious and disciplined in his work. When faith "
80
+ f"or values come up, you can mention this naturally, but keep the focus on "
81
+ f"respectful and professional conversation.\n\n"
82
+ f"If you don't know the answer to any question, use your "
83
+ f"'record_unknown_question' tool to record the question, even if it's "
84
+ f"trivial or unrelated to career.\n\n"
85
+ f"If the user seems like a potential employer, client, or collaborator, "
86
+ f"gently encourage them to share their email so {self.name} can follow up, "
87
+ f"and record it using your 'record_user_details' tool.\n"
88
+ )
89
+
90
+ system_prompt += f"\n## Short Summary\n{self.summary}\n"
91
+ system_prompt += f"\n## LinkedIn-style Profile\n{self.linkedin}\n"
92
+ system_prompt += f"\n## Additional Personal Context\n{self.extra_context}\n"
93
+ system_prompt += (
94
+ "\nWith this context, please chat with the user, always staying in "
95
+ f"character as {self.name}."
96
+ )
97
+ return system_prompt
98
+
99
+
100
+ def _run_conversation(self, message, history_messages):
101
+ """
102
+ Internal helper that runs the full tool-calling loop and returns
103
+ the final assistant message content as a string.
104
+ `history_messages` is a list of OpenAI-style dicts: [{role, content}, ...]
105
+ (no system message inside; we add it here).
106
+ """
107
+ if history_messages is None:
108
+ history_messages = []
109
+
110
+ messages = [
111
+ {"role": "system", "content": self.system_prompt()}
112
+ ] + history_messages + [{"role": "user", "content": message}]
113
+
114
+ done = False
115
+ while not done:
116
+ response = self.openai.chat.completions.create(
117
+ model="gpt-4o-mini",
118
+ messages=messages,
119
+ tools=TOOLS,
120
+ )
121
+ choice = response.choices[0]
122
+ if choice.finish_reason == "tool_calls":
123
+ message_tool = choice.message
124
+ tool_calls = message_tool.tool_calls
125
+ results = self.handle_tool_call(tool_calls)
126
+ messages.append(message_tool)
127
+ messages.extend(results)
128
+ else:
129
+ done = True
130
+ final_message = choice.message
131
+ return final_message.content or ""
132
+
133
+ return ""
134
+
135
+
136
+ def chat_stream(self, message, history_messages):
137
+ """
138
+ Generator version for streaming.
139
+ Yields *partial assistant content* as a plain string.
140
+ Gradio can wrap this to update the Chatbot incrementally.
141
+ """
142
+ full_content = self._run_conversation(message, history_messages)
143
+
144
+ partial = ""
145
+ for ch in full_content:
146
+ partial += ch
147
+ yield partial
148
+
149
+
150
+ def chat(self, message, history_messages):
151
+ """
152
+ Non-streaming wrapper kept for compatibility with existing code.
153
+ It internally uses `chat_stream` and just returns the final string.
154
+ """
155
+ last_chunk = ""
156
+ for chunk in self.chat_stream(message, history_messages):
157
+ last_chunk = chunk
158
+ return last_chunk
logic/tools.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv(override=True)
6
+
7
+
8
+ def push(text: str):
9
+ """Send a notification via Pushover when credentials are configured."""
10
+ if not os.getenv("PUSHOVER_TOKEN") or not os.getenv("PUSHOVER_USER"):
11
+ # Fail silently if pushover is not configured – avoids runtime errors
12
+ print("[PUSHOVER] Not configured, message was:", text, flush=True)
13
+ return
14
+ requests.post(
15
+ "https://api.pushover.net/1/messages.json",
16
+ data={
17
+ "token": os.getenv("PUSHOVER_TOKEN"),
18
+ "user": os.getenv("PUSHOVER_USER"),
19
+ "message": text,
20
+ },
21
+ )
22
+
23
+
24
+ def record_user_details(email, name="Name not provided", notes="not provided"):
25
+ push(f"Recording {name} with email {email} and notes {notes}")
26
+ return {"recorded": "ok"}
27
+
28
+
29
+ def record_unknown_question(question):
30
+ push(f"User made an interesting question that I could not answer: {question}")
31
+ return {"recorded": "ok"}
32
+
33
+
34
+ record_user_details_json = {
35
+ "name": "record_user_details",
36
+ "description": "Use this tool to record that a user is interested in being in touch and provided an email address",
37
+ "parameters": {
38
+ "type": "object",
39
+ "properties": {
40
+ "email": {
41
+ "type": "string",
42
+ "description": "The email address of this user",
43
+ },
44
+ "name": {
45
+ "type": "string",
46
+ "description": "The user's name, if they provided it",
47
+ },
48
+ "notes": {
49
+ "type": "string",
50
+ "description": "Any additional information about the conversation that's worth recording to give context",
51
+ },
52
+ },
53
+ "required": ["email"],
54
+ "additionalProperties": False,
55
+ },
56
+ }
57
+
58
+ record_unknown_question_json = {
59
+ "name": "record_unknown_question",
60
+ "description": "Always use this tool to record any question that couldn't be answered as you didn't know the answer",
61
+ "parameters": {
62
+ "type": "object",
63
+ "properties": {
64
+ "question": {
65
+ "type": "string",
66
+ "description": "The question that couldn't be answered",
67
+ },
68
+ },
69
+ "required": ["question"],
70
+ "additionalProperties": False,
71
+ },
72
+ }
73
+
74
+ TOOLS = [
75
+ {"type": "function", "function": record_user_details_json},
76
+ {"type": "function", "function": record_unknown_question_json},
77
+ ]
78
+
79
+ FUNCTION_MAP = {
80
+ "record_user_details": record_user_details,
81
+ "record_unknown_question": record_unknown_question,
82
+ }
83
+
pyproject.toml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "chat-with-evison"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "gradio>=6.1.0",
9
+ "openai>=2.9.0",
10
+ "pypdf>=6.4.1",
11
+ "python-dotenv>=1.2.1",
12
+ "requests>=2.32.5",
13
+ ]
requirements.txt CHANGED
@@ -1,6 +1,5 @@
1
- requests
2
- python-dotenv
3
- gradio
4
- pypdf
5
- openai
6
- openai-agents
 
1
+ gradio
2
+ openai
3
+ python-dotenv
4
+ pypdf
5
+ requests
 
ui/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # UI package
2
+
ui/layout.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ import gradio as gr
4
+
5
+ from logic.profile import Me
6
+
7
+
8
+ def _load_css() -> str:
9
+ css_path = Path(__file__).parent / "styles.css"
10
+ with open(css_path, "r", encoding="utf-8") as f:
11
+ return f.read()
12
+
13
+
14
+ def build_interface():
15
+ me = Me()
16
+ custom_css = _load_css()
17
+
18
+ with gr.Blocks(css=custom_css) as demo:
19
+ with gr.Row(elem_classes="app-row"):
20
+ with gr.Column(scale=3, elem_classes="chat-column"):
21
+ gr.Markdown(
22
+ "### Chat with **Evison Ndoni**",
23
+ elem_classes="title-text",
24
+ )
25
+ chatbot = gr.Chatbot(
26
+ height=520,
27
+ elem_id="chatbot",
28
+ )
29
+ with gr.Row(elem_classes="input-row"):
30
+ msg = gr.Textbox(
31
+ placeholder=(
32
+ "Ask about my background, past work, AI/agentic AI, "
33
+ "or anything career-related..."
34
+ ),
35
+ label="",
36
+ show_label=False,
37
+ elem_id="chat-input",
38
+ )
39
+
40
+ with gr.Row(elem_classes="button-row"):
41
+ send_btn = gr.Button("Send", elem_id="send-btn")
42
+ clear_btn = gr.Button("Clear chat", elem_id="clear-btn")
43
+
44
+ with gr.Column(scale=2, elem_classes="sidebar-column"):
45
+ gr.Markdown(
46
+ """
47
+ ### About Evison Ndoni
48
+
49
+ - Software engineer (React, Next.js, TypeScript, Tailwind CSS, Flutter)
50
+ - Currently learning **Agentic AI** and building AI-powered projects
51
+ - Enjoys clean, SaaS-style product design and long-term thinking
52
+
53
+ If you're a recruiter, hiring manager, or potential collaborator,
54
+ feel free to ask anything about my experience, stack, or projects.
55
+
56
+ You can also **share your email in the chat** if you'd like me to follow up.
57
+ """,
58
+ elem_classes="about-card",
59
+ )
60
+
61
+
62
+ def respond(message, history):
63
+ """
64
+ Gradio 6 Chatbot uses a 'messages' format:
65
+ history is a list of dicts: [{ "role": "user"|"assistant", "content": "..." }, ...]
66
+ We must return the *updated* history in the same format.
67
+ """
68
+ if history is None:
69
+ history = []
70
+
71
+ assistant_reply = me.chat(message, history)
72
+
73
+ new_history = history + [
74
+ {"role": "user", "content": message},
75
+ {"role": "assistant", "content": assistant_reply},
76
+ ]
77
+ return new_history
78
+
79
+ send_btn.click(
80
+ respond,
81
+ inputs=[msg, chatbot],
82
+ outputs=chatbot,
83
+ )
84
+ send_btn.click(lambda: "", inputs=None, outputs=msg)
85
+ clear_btn.click(lambda: [], inputs=None, outputs=chatbot)
86
+
87
+ demo.load(lambda: [], None, chatbot)
88
+
89
+ return demo
ui/styles.css ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ html,
2
+ body,
3
+ .gradio-container {
4
+ background: #0a0f1f !important;
5
+ color: #e5e7eb !important;
6
+ }
7
+
8
+ :root {
9
+ color-scheme: dark;
10
+ }
11
+
12
+ :root {
13
+ --bg-main: #0a0f1f;
14
+ --bg-panel: #0f172a;
15
+ --bg-card: #111827;
16
+ --bg-elevated: #1e293b;
17
+ --bg-input: #0f172a;
18
+
19
+ --text-primary: #e5e7eb;
20
+ --text-secondary: #9ca3af;
21
+ --text-muted: #6b7280;
22
+
23
+ --accent: #22c55e;
24
+ --accent-strong: #16a34a;
25
+
26
+ --border-subtle: rgba(148, 163, 184, 0.18);
27
+
28
+ --radius-lg: 18px;
29
+ --radius-md: 10px;
30
+
31
+ --shadow-soft: 0 18px 40px rgba(0, 0, 0, 0.55);
32
+ --shadow-card: 0 10px 24px rgba(0, 0, 0, 0.45);
33
+ }
34
+
35
+ .app-row {
36
+ max-width: 1180px;
37
+ margin: 50px auto;
38
+ padding: 28px;
39
+ border-radius: 26px;
40
+ background: var(--bg-card);
41
+ border: 1px solid rgba(255, 255, 255, 0.04);
42
+ box-shadow: var(--shadow-soft);
43
+ }
44
+
45
+ .title-text h3,
46
+ .title-text h1 {
47
+ color: var(--text-primary);
48
+ font-weight: 600;
49
+ }
50
+
51
+ #chatbot {
52
+ background: var(--bg-panel) !important;
53
+ border-radius: var(--radius-lg);
54
+ border: 1px solid var(--border-subtle);
55
+ height: 520px !important;
56
+ box-shadow: var(--shadow-card);
57
+ }
58
+
59
+ #chatbot .message.user {
60
+ background: rgba(255, 255, 255, 0.05) !important;
61
+ color: var(--text-primary) !important;
62
+ }
63
+
64
+ #chatbot .message.bot {
65
+ background: rgba(20, 25, 40, 0.8) !important;
66
+ color: var(--text-secondary) !important;
67
+ }
68
+
69
+ #chat-input {
70
+ background: transparent !important;
71
+ border: none !important;
72
+ box-shadow: none !important;
73
+ padding: 0 !important;
74
+ }
75
+
76
+ #chat-input textarea {
77
+ width: 100%;
78
+ background: #020617;
79
+ border: 1px solid rgba(148, 163, 184, 0.35);
80
+ font-size: 0.95rem;
81
+ color: #e5e7eb;
82
+ outline: none;
83
+ box-shadow: none;
84
+ transition: border-color 0.15s ease, box-shadow 0.15s ease,
85
+ background-color 0.15s ease;
86
+ }
87
+
88
+ #chat-input textarea::placeholder {
89
+ color: rgba(148, 163, 184, 0.8);
90
+ }
91
+
92
+ #chat-input textarea:focus {
93
+ background-color: #020617;
94
+ border-color: #585858; /* soft green */
95
+ box-shadow: 0 0 0 1px rgba(34, 197, 94, 0.35);
96
+ }
97
+
98
+ .button-row {
99
+ margin-top: 12px;
100
+ gap: 12px;
101
+ }
102
+
103
+ #send-btn {
104
+ background: linear-gradient(135deg, #1ed760, #1abc5b);
105
+ border: none;
106
+ color: white;
107
+ font-weight: 600;
108
+ border-radius: 999px;
109
+ padding: 0.65rem 1.8rem;
110
+ font-size: 0.95rem;
111
+
112
+ box-shadow: 0 4px 14px rgba(30, 215, 96, 0.25); /* subtle shadow */
113
+ transition: transform 0.12s ease, box-shadow 0.2s ease, filter 0.2s ease;
114
+ }
115
+
116
+ #send-btn:hover {
117
+ transform: translateY(-2px);
118
+ box-shadow: 0 6px 18px rgba(30, 215, 96, 0.32);
119
+ filter: brightness(1.05);
120
+ }
121
+
122
+ #send-btn:active {
123
+ transform: scale(0.97);
124
+ box-shadow: 0 3px 12px rgba(30, 215, 96, 0.2);
125
+ }
126
+
127
+ #clear-btn {
128
+ background: transparent;
129
+ color: var(--text-secondary);
130
+ border: 1px solid rgba(255, 255, 255, 0.12);
131
+ border-radius: 999px;
132
+ padding: 0.65rem 1.8rem;
133
+ }
134
+
135
+ #clear-btn:hover {
136
+ background: rgba(255, 255, 255, 0.06);
137
+ color: var(--text-primary);
138
+ }
139
+
140
+ .about-card {
141
+ background: var(--bg-panel) !important;
142
+ padding: 22px;
143
+ border-radius: var(--radius-lg);
144
+ border: 1px solid rgba(255, 255, 255, 0.06);
145
+ box-shadow: var(--shadow-card);
146
+ color: var(--text-secondary) !important;
147
+ }
148
+
149
+ .about-card h3 {
150
+ color: var(--text-primary);
151
+ margin-bottom: 10px;
152
+ }
153
+
154
+ .about-card li::marker {
155
+ color: var(--accent);
156
+ }
157
+
158
+ @media (max-width: 768px) {
159
+ .app-row {
160
+ margin: 20px;
161
+ padding: 18px;
162
+ }
163
+ }
uv.lock ADDED
The diff for this file is too large to render. See raw diff