omarsol commited on
Commit
fed171e
·
1 Parent(s): 6cb83c3

Add Docker support with .dockerignore and Dockerfile updates. Integrate Next.js frontend build process, including package.json and related configurations. Enhance API to serve static frontend files and update README for project clarity. Add course and document metadata structures for improved content management.

Browse files
.dockerignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Version control
2
+ .git
3
+ .github
4
+
5
+ # Python build artifacts
6
+ __pycache__
7
+ *.pyc
8
+ .venv
9
+ venv
10
+ .pytest_cache
11
+ .mypy_cache
12
+
13
+ # Node build artifacts
14
+ frontend/node_modules
15
+ frontend/.next
16
+ frontend/out
17
+
18
+ # Local env files (secrets / per-developer config)
19
+ .env
20
+ .env.local
21
+ .env.*.local
22
+ frontend/.env
23
+ frontend/.env.local
24
+ frontend/.env.*.local
25
+
26
+ # Editor/OS
27
+ .vscode
28
+ .idea
29
+ .DS_Store
30
+
31
+ # Docs and tests (optional – shrink build context)
32
+ docs
33
+ tests
34
+ *.md
35
+ !README.md
36
+
37
+ # Heavy data that will be downloaded at runtime from HF Hub
38
+ data/chroma-db-all_sources
39
+ data/*.jsonl
40
+ data/*.pkl
41
+ data/*_md_files
.github/workflows/sync-to-hf.yml CHANGED
@@ -24,4 +24,4 @@ jobs:
24
  env:
25
  HF_TOKEN: ${{ secrets.HF_TOKEN }}
26
  HF_USERNAME: ${{ secrets.HF_USERNAME }}
27
- run: git push --force https://$HF_USERNAME:$HF_TOKEN@huggingface.co/spaces/towardsai-tutors/ai-tutor-chatbot main:main
 
24
  env:
25
  HF_TOKEN: ${{ secrets.HF_TOKEN }}
26
  HF_USERNAME: ${{ secrets.HF_USERNAME }}
27
+ run: git push --force https://$HF_USERNAME:$HF_TOKEN@huggingface.co/spaces/towardsai-tutors/ai-tutor main:main
.gitignore CHANGED
@@ -14,8 +14,8 @@ dist/
14
  downloads/
15
  eggs/
16
  .eggs/
17
- lib/
18
- lib64/
19
  parts/
20
  sdist/
21
  var/
@@ -182,3 +182,12 @@ frontend/out/
182
  *.pkl
183
  *.png
184
  *.mov
 
 
 
 
 
 
 
 
 
 
14
  downloads/
15
  eggs/
16
  .eggs/
17
+ /lib/
18
+ /lib64/
19
  parts/
20
  sdist/
21
  var/
 
182
  *.pkl
183
  *.png
184
  *.mov
185
+
186
+ # Keep assets + configs needed for deployment
187
+ !frontend/package.json
188
+ !frontend/package-lock.json
189
+ !frontend/tsconfig.json
190
+ !frontend/public/towardsai-logo.png
191
+ !frontend/app/icon.png
192
+ !frontend/app/apple-icon.png
193
+ !data/source_versions.json
Dockerfile CHANGED
@@ -1,7 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  FROM python:3.13
2
 
3
  RUN curl -LsSf https://astral.sh/uv/install.sh | sh
4
-
5
  ENV PATH="/root/.local/bin:${PATH}"
6
 
7
  RUN useradd -m -u 1000 user
@@ -11,11 +27,13 @@ COPY pyproject.toml uv.lock ./
11
  RUN uv sync --locked
12
 
13
  COPY . .
 
14
 
15
-
16
- ENV HOME=/home/user PATH="/root/.local/bin:/home/user/.local/bin:${PATH}"
 
17
  RUN chown -R user:user /app
18
  USER user
19
 
20
  EXPOSE 7860
21
- CMD ["uv", "run", "-m", "scripts.main"]
 
1
+ # syntax=docker/dockerfile:1.6
2
+
3
+ # Stage 1: build the Next.js frontend as a static export
4
+ FROM node:20-alpine AS frontend
5
+ WORKDIR /frontend
6
+
7
+ COPY frontend/package.json frontend/package-lock.json ./
8
+ RUN npm ci
9
+
10
+ COPY frontend/ ./
11
+
12
+ # Empty base URL => API calls are same-origin (served by FastAPI)
13
+ ENV NEXT_PUBLIC_AI_TUTOR_API_BASE_URL=""
14
+ RUN npm run build
15
+
16
+
17
+ # Stage 2: Python runtime + FastAPI + bundled static frontend
18
  FROM python:3.13
19
 
20
  RUN curl -LsSf https://astral.sh/uv/install.sh | sh
 
21
  ENV PATH="/root/.local/bin:${PATH}"
22
 
23
  RUN useradd -m -u 1000 user
 
27
  RUN uv sync --locked
28
 
29
  COPY . .
30
+ COPY --from=frontend /frontend/out ./frontend/out
31
 
32
+ ENV HOME=/home/user \
33
+ PATH="/root/.local/bin:/home/user/.local/bin:${PATH}" \
34
+ PORT=7860
35
  RUN chown -R user:user /app
36
  USER user
37
 
38
  EXPOSE 7860
39
+ CMD ["sh", "-c", "uv run uvicorn scripts.api:app --host 0.0.0.0 --port ${PORT:-7860}"]
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
- title: AI Tutor Chatbot
3
- emoji: 🧑🏻‍🏫
4
- colorFrom: gray
5
- colorTo: pink
6
  sdk: docker
7
  app_port: 7860
8
  pinned: false
 
1
  ---
2
+ title: AI Tutor
3
+ emoji: 🎓
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
  app_port: 7860
8
  pinned: false
data/source_versions.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "langchain": {
3
+ "indexedAt": "2026-04-17",
4
+ "sha": "b921b07",
5
+ "version": "langchain-core==1.3.0"
6
+ },
7
+ "llama_index": {
8
+ "indexedAt": "2026-04-17",
9
+ "sha": "33f7ba4",
10
+ "version": "v0.14.20"
11
+ },
12
+ "openai_cookbooks": {
13
+ "indexedAt": "2026-04-17",
14
+ "sha": "9e0a00d",
15
+ "version": null
16
+ },
17
+ "peft": {
18
+ "indexedAt": "2026-04-17",
19
+ "sha": "2859358",
20
+ "version": "v0.19.1"
21
+ },
22
+ "transformers": {
23
+ "indexedAt": "2026-04-17",
24
+ "sha": "a29df2d",
25
+ "version": "v5.5.4"
26
+ },
27
+ "trl": {
28
+ "indexedAt": "2026-04-17",
29
+ "sha": "573ea22",
30
+ "version": "v1.2.0"
31
+ }
32
+ }
frontend/app/apple-icon.png ADDED
frontend/app/icon.png ADDED
frontend/lib/api.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type TutorSource = {
2
+ key: string;
3
+ label: string;
4
+ group: "courses" | "docs";
5
+ selectedByDefault: boolean;
6
+ version?: string | null;
7
+ indexedAt?: string | null;
8
+ };
9
+
10
+ export type TutorConfigurableTool = {
11
+ kind: "configurable";
12
+ key: "retrieval";
13
+ label: string;
14
+ active: boolean;
15
+ sources: TutorSource[];
16
+ };
17
+
18
+ export type TutorToggleTool = {
19
+ kind: "toggle";
20
+ key: string;
21
+ label: string;
22
+ active: boolean;
23
+ };
24
+
25
+ export type TutorTool = TutorConfigurableTool | TutorToggleTool;
26
+
27
+ export type AvailableModel = {
28
+ id: string;
29
+ label: string;
30
+ };
31
+
32
+ export type ToolsResponse = {
33
+ model: string;
34
+ availableModels: AvailableModel[];
35
+ tools: TutorTool[];
36
+ };
37
+
38
+ export type SourcePartData = {
39
+ docId: string;
40
+ title: string;
41
+ url: string;
42
+ sourceKey: string;
43
+ sourceLabel: string;
44
+ score: number;
45
+ };
46
+
47
+ const FALLBACK_API_BASE_URL = "http://127.0.0.1:8000";
48
+
49
+ export function getApiBaseUrl() {
50
+ return (
51
+ process.env.NEXT_PUBLIC_AI_TUTOR_API_BASE_URL?.replace(/\/+$/, "") ??
52
+ FALLBACK_API_BASE_URL
53
+ );
54
+ }
55
+
56
+ export async function fetchTools(
57
+ signal?: AbortSignal,
58
+ model?: string,
59
+ ): Promise<ToolsResponse> {
60
+ const url = new URL(`${getApiBaseUrl()}/api/tools`);
61
+ if (model) {
62
+ url.searchParams.set("model", model);
63
+ }
64
+ const response = await fetch(url.toString(), {
65
+ method: "GET",
66
+ signal,
67
+ });
68
+
69
+ if (!response.ok) {
70
+ throw new Error(`Unable to load tools (${response.status})`);
71
+ }
72
+
73
+ return (await response.json()) as ToolsResponse;
74
+ }
frontend/lib/chat-ui.ts ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { UIMessage } from "ai";
2
+ import type { SourcePartData } from "./api";
3
+
4
+ export type TutorMessage = UIMessage;
5
+
6
+ export type TutorMessagePart = {
7
+ type: string;
8
+ text?: string;
9
+ state?: string;
10
+ data?: unknown;
11
+ toolCallId?: string;
12
+ input?: unknown;
13
+ output?: unknown;
14
+ errorText?: string;
15
+ sourceId?: string;
16
+ url?: string;
17
+ title?: string;
18
+ mediaType?: string;
19
+ };
20
+
21
+ export type TutorMessageBlock = {
22
+ key: string;
23
+ kind: "text" | "reasoning" | "tool";
24
+ parts: TutorMessagePart[];
25
+ };
26
+
27
+ export type CitationKind = "web" | "course" | "doc";
28
+
29
+ export type MessageCitation = {
30
+ label: string;
31
+ url: string;
32
+ kind: CitationKind;
33
+ sublabel?: string;
34
+ };
35
+
36
+ export function getTextParts(message: TutorMessage) {
37
+ return message.parts
38
+ .filter((part) => "type" in part && part.type === "text")
39
+ .map((part) => part as TutorMessagePart);
40
+ }
41
+
42
+ export function getMessageTextContent(message: TutorMessage) {
43
+ return getTextParts(message)
44
+ .map((part) => (part.text ?? "").trim())
45
+ .filter(Boolean)
46
+ .join("\n\n");
47
+ }
48
+
49
+ export function getReasoningParts(message: TutorMessage) {
50
+ return message.parts
51
+ .filter((part) => "type" in part && part.type === "reasoning")
52
+ .map((part) => part as TutorMessagePart);
53
+ }
54
+
55
+ export function getToolParts(message: TutorMessage) {
56
+ return message.parts
57
+ .filter((part) => "type" in part && String(part.type).startsWith("tool-"))
58
+ .map((part) => part as TutorMessagePart);
59
+ }
60
+
61
+ export function getOrderedMessageBlocks(message: TutorMessage): TutorMessageBlock[] {
62
+ const blocks: TutorMessageBlock[] = [];
63
+ let currentKind: TutorMessageBlock["kind"] | null = null;
64
+ let currentParts: TutorMessagePart[] = [];
65
+
66
+ for (const part of message.parts) {
67
+ if (!("type" in part)) {
68
+ continue;
69
+ }
70
+
71
+ const typedPart = part as TutorMessagePart;
72
+ const nextKind = classifyMessagePart(typedPart);
73
+
74
+ if (!nextKind) {
75
+ continue;
76
+ }
77
+
78
+ if (currentKind && currentKind !== nextKind) {
79
+ blocks.push({
80
+ key: `${message.id}-${currentKind}-${blocks.length}`,
81
+ kind: currentKind,
82
+ parts: currentParts,
83
+ });
84
+ currentParts = [];
85
+ }
86
+
87
+ currentKind = nextKind;
88
+ currentParts.push(typedPart);
89
+ }
90
+
91
+ if (currentKind && currentParts.length > 0) {
92
+ blocks.push({
93
+ key: `${message.id}-${currentKind}-${blocks.length}`,
94
+ kind: currentKind,
95
+ parts: currentParts,
96
+ });
97
+ }
98
+
99
+ return blocks;
100
+ }
101
+
102
+ export function getMessageSources(message: TutorMessage): SourcePartData[] {
103
+ const seen = new Set<string>();
104
+ const sources: SourcePartData[] = [];
105
+
106
+ for (const part of message.parts) {
107
+ if (!("type" in part) || part.type !== "data-source") {
108
+ continue;
109
+ }
110
+ const data = (part as TutorMessagePart).data as SourcePartData | undefined;
111
+ if (!data) {
112
+ continue;
113
+ }
114
+ const key = data.docId || data.url;
115
+ if (seen.has(key)) {
116
+ continue;
117
+ }
118
+ seen.add(key);
119
+ sources.push(data);
120
+ }
121
+
122
+ return sources.sort((left, right) => right.score - left.score);
123
+ }
124
+
125
+ export function hasRenderableContent(message: TutorMessage): boolean {
126
+ for (const part of message.parts) {
127
+ if (!("type" in part)) {
128
+ continue;
129
+ }
130
+ const type = String(part.type);
131
+ if (type === "text" || type === "reasoning" || type.startsWith("tool-")) {
132
+ return true;
133
+ }
134
+ }
135
+ return false;
136
+ }
137
+
138
+ export function getLastAssistantMessage(messages: TutorMessage[]) {
139
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
140
+ if (messages[index]?.role === "assistant") {
141
+ return messages[index];
142
+ }
143
+ }
144
+
145
+ return undefined;
146
+ }
147
+
148
+ const WEB_SOURCE_KEYS = new Set(["google_search", "url_context"]);
149
+ const COURSE_SOURCE_KEYS = new Set([
150
+ "8-hour_primer",
151
+ "llm_developer",
152
+ "python_primer",
153
+ "master_ai_for_work",
154
+ "agentic_ai_engineering",
155
+ ]);
156
+
157
+ function classifySource(sourceKey: string): CitationKind {
158
+ if (WEB_SOURCE_KEYS.has(sourceKey)) {
159
+ return "web";
160
+ }
161
+ if (COURSE_SOURCE_KEYS.has(sourceKey)) {
162
+ return "course";
163
+ }
164
+ return "doc";
165
+ }
166
+
167
+ export function getMessageCitations(message: TutorMessage): MessageCitation[] {
168
+ const seen = new Set<string>();
169
+ const citations: MessageCitation[] = [];
170
+
171
+ for (const source of getMessageSources(message)) {
172
+ const url = source.url?.trim();
173
+ if (!url) {
174
+ continue;
175
+ }
176
+ const kind = classifySource(source.sourceKey);
177
+ const title = cleanTitle(source.title);
178
+ const label = (
179
+ kind === "web"
180
+ ? title || hostnameFromUrl(url) || "Source"
181
+ : title || source.sourceLabel || "Source"
182
+ ).slice(0, 80);
183
+ const sublabel =
184
+ kind === "web"
185
+ ? hostnameFromUrl(url)
186
+ : source.sourceLabel?.trim() || undefined;
187
+ const key = `${kind}::${url}`;
188
+ if (seen.has(key)) {
189
+ continue;
190
+ }
191
+ seen.add(key);
192
+ citations.push({ label, url, kind, sublabel });
193
+ }
194
+
195
+ for (const part of getTextParts(message)) {
196
+ const text = part.text ?? "";
197
+ for (const citation of extractMarkdownLinkCitations(text)) {
198
+ const key = `web::${citation.url}`;
199
+ if (seen.has(key)) {
200
+ continue;
201
+ }
202
+ seen.add(key);
203
+ citations.push({
204
+ label: citation.label,
205
+ url: citation.url,
206
+ kind: "web",
207
+ sublabel: hostnameFromUrl(citation.url) || undefined,
208
+ });
209
+ }
210
+ }
211
+
212
+ return citations;
213
+ }
214
+
215
+ function cleanTitle(raw: string | undefined) {
216
+ const trimmed = raw?.trim();
217
+ if (!trimmed) {
218
+ return "";
219
+ }
220
+ if (/^[-–—\s]+$/.test(trimmed)) {
221
+ return "";
222
+ }
223
+ return trimmed;
224
+ }
225
+
226
+ function hostnameFromUrl(url: string) {
227
+ try {
228
+ return new URL(url).hostname.replace(/^www\./, "");
229
+ } catch {
230
+ return "";
231
+ }
232
+ }
233
+
234
+ export function prettifyToolName(type: string) {
235
+ return type
236
+ .replace(/^tool-/, "")
237
+ .replaceAll("_", " ")
238
+ .replace(/\b\w/g, (match) => match.toUpperCase());
239
+ }
240
+
241
+ export function toolInputSummary(input: unknown) {
242
+ if (!input || typeof input !== "object") {
243
+ return "";
244
+ }
245
+
246
+ const query = "query" in input ? String(input.query ?? "").trim() : "";
247
+ if (query) {
248
+ return query;
249
+ }
250
+
251
+ const text = "text" in input ? String(input.text ?? "").trim() : "";
252
+ if (text) {
253
+ return text;
254
+ }
255
+
256
+ return JSON.stringify(input);
257
+ }
258
+
259
+ function classifyMessagePart(
260
+ part: TutorMessagePart,
261
+ ): TutorMessageBlock["kind"] | null {
262
+ if (part.type === "text") {
263
+ return "text";
264
+ }
265
+
266
+ if (part.type === "reasoning") {
267
+ return "reasoning";
268
+ }
269
+
270
+ if (String(part.type).startsWith("tool-")) {
271
+ return "tool";
272
+ }
273
+
274
+ return null;
275
+ }
276
+
277
+ type RawLinkCitation = { label: string; url: string };
278
+
279
+ function extractMarkdownLinkCitations(text: string): RawLinkCitation[] {
280
+ const citations: RawLinkCitation[] = [];
281
+ const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+(?:\([^\s)]+\)[^\s)]*)*)\)/g;
282
+ const bareUrlPattern = /(?<!\()https?:\/\/[^\s<>()]+/g;
283
+ const urlsInMarkdown = new Set<string>();
284
+
285
+ for (const match of text.matchAll(markdownLinkPattern)) {
286
+ const label = (match[1] ?? "").trim();
287
+ const url = (match[2] ?? "").trim();
288
+ if (!label || !url) {
289
+ continue;
290
+ }
291
+ urlsInMarkdown.add(url);
292
+ citations.push({ label, url });
293
+ }
294
+
295
+ for (const match of text.matchAll(bareUrlPattern)) {
296
+ const url = (match[0] ?? "").trim();
297
+ if (!url || urlsInMarkdown.has(url)) {
298
+ continue;
299
+ }
300
+ citations.push({ label: "Source", url });
301
+ }
302
+
303
+ return citations;
304
+ }
frontend/lib/course-metadata.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type CourseMetadata = {
2
+ description: string;
3
+ academyUrl: string;
4
+ };
5
+
6
+ export const COURSE_METADATA: Record<string, CourseMetadata> = {
7
+ "8-hour_primer": {
8
+ description:
9
+ "Crash course on LLM fundamentals: prompting, APIs, and shipping simple production apps. Good starting point for beginners deciding if LLM dev is for them.",
10
+ academyUrl: "https://academy.towardsai.net/courses/8-hour-genai-primer",
11
+ },
12
+ full_stack_ai_engineering: {
13
+ description:
14
+ "Full-stack LLM engineering, covering RAG, fine-tuning, evaluation, and deploying production systems end-to-end. The deepest technical course.",
15
+ academyUrl:
16
+ "https://academy.towardsai.net/courses/beginner-to-advanced-llm-dev",
17
+ },
18
+ beginner_python_for_ai_engineering: {
19
+ description:
20
+ "Python for the LLM era: API integration, using open-source models, and core training/testing workflows. Assumes no prior Python.",
21
+ academyUrl: "https://academy.towardsai.net/courses/python-for-genai",
22
+ },
23
+ master_ai_for_work: {
24
+ description:
25
+ "Non-engineer course on using AI tools (ChatGPT, Claude, etc.) for workplace productivity and rolling them out across a team.",
26
+ academyUrl:
27
+ "https://academy.towardsai.net/courses/ai-business-professionals",
28
+ },
29
+ agentic_ai_engineering: {
30
+ description:
31
+ "Designing, building, evaluating, and deploying production-grade AI agents end-to-end.",
32
+ academyUrl: "https://academy.towardsai.net/courses/agent-engineering",
33
+ },
34
+ };
frontend/lib/doc-metadata.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type DocMetadata = {
2
+ description: string;
3
+ docsUrl: string;
4
+ };
5
+
6
+ export const DOC_METADATA: Record<string, DocMetadata> = {
7
+ transformers: {
8
+ description:
9
+ "Hugging Face library for state-of-the-art NLP and multimodal models. Load, run, and train pretrained transformers.",
10
+ docsUrl: "https://huggingface.co/docs/transformers",
11
+ },
12
+ peft: {
13
+ description:
14
+ "Parameter-Efficient Fine-Tuning: LoRA, prefix tuning, and other methods for adapting large models with minimal compute.",
15
+ docsUrl: "https://huggingface.co/docs/peft",
16
+ },
17
+ trl: {
18
+ description:
19
+ "Train language models with reinforcement learning. Covers SFT, DPO, PPO, and other alignment techniques.",
20
+ docsUrl: "https://huggingface.co/docs/trl",
21
+ },
22
+ llama_index: {
23
+ description:
24
+ "Framework for building RAG apps: ingestion, indexing, retrievers, and query engines over your own data.",
25
+ docsUrl: "https://docs.llamaindex.ai",
26
+ },
27
+ langchain: {
28
+ description:
29
+ "Framework for building LLM apps: chains, agents, tool-calling, and production observability.",
30
+ docsUrl: "https://docs.langchain.com/oss/python/langchain/overview",
31
+ },
32
+ openai_cookbooks: {
33
+ description:
34
+ "Example notebooks and recipes from OpenAI covering practical patterns for using their APIs.",
35
+ docsUrl: "https://cookbook.openai.com",
36
+ },
37
+ };
frontend/next-env.d.ts CHANGED
@@ -1,6 +1,6 @@
1
  /// <reference types="next" />
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/dev/types/routes.d.ts";
4
 
5
  // NOTE: This file should not be edited
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
 
1
  /// <reference types="next" />
2
  /// <reference types="next/image-types/global" />
3
+ import "./.next/types/routes.d.ts";
4
 
5
  // NOTE: This file should not be edited
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
frontend/next.config.mjs CHANGED
@@ -6,6 +6,9 @@ const __dirname = dirname(__filename);
6
 
7
  /** @type {import('next').NextConfig} */
8
  const nextConfig = {
 
 
 
9
  turbopack: {
10
  root: __dirname,
11
  },
 
6
 
7
  /** @type {import('next').NextConfig} */
8
  const nextConfig = {
9
+ output: "export",
10
+ images: { unoptimized: true },
11
+ trailingSlash: true,
12
  turbopack: {
13
  root: __dirname,
14
  },
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ai-tutor-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "packageManager": "npm@11.12.1",
6
+ "scripts": {
7
+ "dev": "next dev",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "lint": "eslint ."
11
+ },
12
+ "dependencies": {
13
+ "@ai-sdk/react": "^3.0.164",
14
+ "ai": "^6.0.162",
15
+ "clsx": "^2.1.1",
16
+ "lucide-react": "^0.511.0",
17
+ "next": "^16.2.3",
18
+ "react": "^19.2.0",
19
+ "react-dom": "^19.2.0",
20
+ "react-markdown": "^10.1.0",
21
+ "remark-gfm": "^4.0.1"
22
+ },
23
+ "devDependencies": {
24
+ "@eslint/eslintrc": "^3.3.1",
25
+ "@tailwindcss/postcss": "^4.1.17",
26
+ "@types/node": "^24.10.1",
27
+ "@types/react": "^19.2.2",
28
+ "@types/react-dom": "^19.2.2",
29
+ "eslint": "^9.39.1",
30
+ "eslint-config-next": "^16.2.3",
31
+ "tailwindcss": "^4.2.2",
32
+ "typescript": "^5.9.3"
33
+ }
34
+ }
frontend/public/towardsai-logo.png ADDED
frontend/tsconfig.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": false,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "noEmit": true,
13
+ "esModuleInterop": true,
14
+ "module": "esnext",
15
+ "moduleResolution": "bundler",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "jsx": "react-jsx",
19
+ "incremental": true,
20
+ "plugins": [
21
+ {
22
+ "name": "next"
23
+ }
24
+ ],
25
+ "paths": {
26
+ "@/*": [
27
+ "./*"
28
+ ]
29
+ }
30
+ },
31
+ "include": [
32
+ "next-env.d.ts",
33
+ "**/*.ts",
34
+ "**/*.tsx",
35
+ ".next/types/**/*.ts",
36
+ ".next/dev/types/**/*.ts"
37
+ ],
38
+ "exclude": [
39
+ "node_modules"
40
+ ]
41
+ }
scripts/api.py CHANGED
@@ -12,6 +12,7 @@ import uvicorn
12
  from fastapi import FastAPI, HTTPException
13
  from fastapi.middleware.cors import CORSMiddleware
14
  from fastapi.responses import StreamingResponse
 
15
  from pydantic import BaseModel, Field
16
 
17
  from .chat_service import (
@@ -462,6 +463,21 @@ async def chat(payload: ApiChatRequest) -> StreamingResponse:
462
  )
463
 
464
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  if __name__ == "__main__":
466
  uvicorn.run(
467
  "scripts.api:app",
 
12
  from fastapi import FastAPI, HTTPException
13
  from fastapi.middleware.cors import CORSMiddleware
14
  from fastapi.responses import StreamingResponse
15
+ from fastapi.staticfiles import StaticFiles
16
  from pydantic import BaseModel, Field
17
 
18
  from .chat_service import (
 
463
  )
464
 
465
 
466
+ FRONTEND_OUT_DIR = Path(__file__).resolve().parent.parent / "frontend" / "out"
467
+
468
+ if FRONTEND_OUT_DIR.is_dir():
469
+ app.mount(
470
+ "/",
471
+ StaticFiles(directory=str(FRONTEND_OUT_DIR), html=True),
472
+ name="frontend",
473
+ )
474
+ else:
475
+ logfire.info(
476
+ "Frontend static export not found; skipping static mount.",
477
+ path=str(FRONTEND_OUT_DIR),
478
+ )
479
+
480
+
481
  if __name__ == "__main__":
482
  uvicorn.run(
483
  "scripts.api:app",