Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ alwaysApply: true
- [GT1a-l] Git, history safety, hooks/signing, lock files, and clean commits
- [CC1a-d] Clean Code & DDD (Mandatory)
- [ID1a-d] Idiomatic Patterns & Defaults
- [EV1a-f] Secrets and Env Vars (no secrets in properties; OpenAI/Qdrant via .env/env)
- [RC1a-f] Root Cause Resolution (single implementation, no fallbacks, no shims/workarounds)
- [FS1a-k] File Creation & Clean Architecture (search first, strict types, single responsibility)
- [TY1a-d] Type Safety (strict generics, no raw types, no unchecked casts)
Expand Down Expand Up @@ -70,6 +71,17 @@ alwaysApply: true
- [ID1c] **No Reinventing**: Do not build custom utilities for things the platform already does (e.g., use standard `Optional`, `Stream`, Spring `RestTemplate`/`WebClient`).
- [ID1d] **Dependencies**: Make careful use of dependencies. Do not make assumptions—use the correct idiomatic behavior to avoid boilerplate.

## [EV1] Secrets and Env Vars

- [EV1a] **No Secrets In Properties/YAML**: Secrets are PROHIBITED in `.properties` and `.yml/.yaml` (including examples). Do not add secret-looking keys with blank defaults to property files.
- [EV1b] **Secrets Source Of Truth**: Secrets MUST be provided via `.env` (local) and environment variables (deployment). Deployment uses Coolify containers with BuildKit secrets; keep secrets out of tracked config.
- [EV1c] **Non-Secret Defaults In Properties**: All non-secret defaults and environment-specific overrides MUST live in Spring property files (`src/main/resources/application*.properties`) and Spring profiles.
- [EV1d] **Allowed `.env`/Env Vars For External Services**: OpenAI/GitHub Models and Qdrant connectivity MUST come from `.env`/environment variables:
- LLM (model/base-url/api-key): `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`, `GITHUB_TOKEN`, `GITHUB_MODELS_BASE_URL`, `GITHUB_MODELS_CHAT_MODEL`
- Qdrant (host/ports/tls/api-key): `QDRANT_HOST`, `QDRANT_PORT`, `QDRANT_REST_PORT`, `QDRANT_SSL`, `QDRANT_API_KEY`
- [EV1e] **No New Env Var Settings**: Do not introduce any additional env-var-driven settings without explicit written approval.
- [EV1f] **Dotenv Handling**: `.env` loading MUST remain supported for local development and scripts. Do not add dotenv libraries into the Java runtime; load env vars at the process level (Makefile/scripts/Coolify).

## [RC1] Root Cause Resolution — No Fallbacks

- [RC1a] **One Way**: Ship one proven implementation—no fallback paths, no "try X then Y", no silent degradation.
Expand Down Expand Up @@ -183,13 +195,13 @@ alwaysApply: true
- [TL1b] **Docker**: `docker compose up -d` for Qdrant vector store.
- [TL1c] **Ingest**: `curl -X POST http://localhost:8080/api/ingest ...`.
- [TL1d] **Stream**: `curl -N http://localhost:8080/api/chat/stream ...`.
- [TL1e] **Secrets**: `.env` for secrets (`GITHUB_TOKEN`, `QDRANT_URL`); never commit secrets.
- [TL1e] **Secrets**: Never commit secrets. Secrets MUST live in `.env` (local) and environment variables (deployment). Secrets are PROHIBITED in `.properties`/`.yml` files.

## [LM1] LLM & Streaming

- [LM1a] **Settings**: Do not change any LLM settings without explicit written approval.
- [LM1b] **No Fallback**: Do not auto-fallback or regress models across providers; surface error to user.
- [LM1c] **Config**: Use values from environment variables and `application.properties` exactly as configured.
- [LM1c] **Config**: OpenAI/GitHub Models model/base-url/api-key MUST come from `.env`/environment variables (see [EV1d]). All other LLM settings MUST come from Spring property files and `@ConfigurationProperties`.
- [LM1d] **Behavior**: Allowed: logging diagnostics. Not allowed: silently changing LLM behavior.
- [LM1e] **Streaming**: TTFB < 200ms, streaming start < 500ms.
- [LM1f] **Events**: `text`, `citation`, `code`, `enrichment`, `suggestion`, `status`.
Expand Down
9 changes: 6 additions & 3 deletions frontend/config/oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@
"rules": {
"no-empty": ["error", { "allowEmptyCatch": false }],
"no-await-in-loop": "off",
"import/no-unassigned-import": ["error", {
"allow": ["**/*.css", "@testing-library/**"]
}],
"import/no-unassigned-import": [
"error",
{
"allow": ["**/*.css", "@testing-library/**"]
}
],
"typescript/no-explicit-any": "error",
"typescript/no-unsafe-type-assertion": "warn",
"typescript/no-floating-promises": "warn",
Expand Down
141 changes: 81 additions & 60 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
Expand All @@ -12,7 +12,10 @@
name="description"
content="Learn Java faster with an AI tutor: streaming answers, code examples, and citations to official docs."
/>
<meta name="robots" content="index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1" />
<meta
name="robots"
content="index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1"
/>
<meta name="referrer" content="strict-origin-when-cross-origin" />

<title>Java Chat - AI-Powered Java Learning With Citations</title>
Expand All @@ -22,21 +25,32 @@
<meta property="og:site_name" content="Java Chat" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />
<meta property="og:title" content="Java Chat - AI-Powered Java Learning With Citations" data-seo="og:title" />
<meta
property="og:title"
content="Java Chat - AI-Powered Java Learning With Citations"
data-seo="og:title"
/>
<meta
property="og:description"
content="Learn Java faster with an AI tutor: streaming answers, code examples, and citations to official docs."
data-seo="og:description"
/>
<meta property="og:url" content="/" data-seo="og:url" />
<meta property="og:image" content="/og-image.png" data-seo="og:image" />
<meta property="og:image:alt" content="JavaChat.ai – Learn programming and chat directly with libraries, SDKs, and GitHub repositories" />
<meta
property="og:image:alt"
content="JavaChat.ai – Learn programming and chat directly with libraries, SDKs, and GitHub repositories"
/>
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:type" content="image/png" />

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Java Chat - AI-Powered Java Learning With Citations" data-seo="twitter:title" />
<meta
name="twitter:title"
content="Java Chat - AI-Powered Java Learning With Citations"
data-seo="twitter:title"
/>
<meta
name="twitter:description"
content="Learn Java faster with an AI tutor: streaming answers, code examples, and citations to official docs."
Expand Down Expand Up @@ -72,91 +86,98 @@
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=DM+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>

<!--
Lightweight SEO: keep reasonable defaults in static HTML, then refine per-path using a tiny inline script.
This improves social previews + canonical URLs for non-root entries served by the SPA fallback.
-->
<script type="application/ld+json" id="java-chat-structured-data"></script>
<script>
;(function () {
var origin = globalThis.location.origin
var path = globalThis.location.pathname || '/'
var normalizedPath = path.endsWith('/') && path.length > 1 ? path.slice(0, -1) : path
(function () {
var origin = globalThis.location.origin;
var path = globalThis.location.pathname || "/";
var normalizedPath = path.endsWith("/") && path.length > 1 ? path.slice(0, -1) : path;

var baseSeo = {
title: 'Java Chat - AI-Powered Java Learning With Citations',
title: "Java Chat - AI-Powered Java Learning With Citations",
description:
'Learn Java faster with an AI tutor: streaming answers, code examples, and citations to official docs.',
imagePath: '/og-image.png'
}
"Learn Java faster with an AI tutor: streaming answers, code examples, and citations to official docs.",
imagePath: "/og-image.png",
};

var perPathSeo = {
'/': baseSeo,
'/chat': {
title: 'Java Chat - Streaming Java Tutor With Citations',
"/": baseSeo,
"/chat": {
title: "Java Chat - Streaming Java Tutor With Citations",
description:
'Ask Java questions and get streaming answers with citations to official docs and practical examples.',
imagePath: baseSeo.imagePath
"Ask Java questions and get streaming answers with citations to official docs and practical examples.",
imagePath: baseSeo.imagePath,
},
'/guided': {
title: 'Guided Java Learning - Java Chat',
description: 'Structured, step-by-step Java learning paths with examples and explanations.',
imagePath: baseSeo.imagePath
"/guided": {
title: "Guided Java Learning - Java Chat",
description:
"Structured, step-by-step Java learning paths with examples and explanations.",
imagePath: baseSeo.imagePath,
},
'/learn': {
title: 'Guided Java Learning - Java Chat',
description: 'Structured, step-by-step Java learning paths with examples and explanations.',
imagePath: baseSeo.imagePath
}
}
"/learn": {
title: "Guided Java Learning - Java Chat",
description:
"Structured, step-by-step Java learning paths with examples and explanations.",
imagePath: baseSeo.imagePath,
},
};

var selectedSeo = perPathSeo[normalizedPath] || baseSeo
var canonicalUrl = origin + normalizedPath
var selectedSeo = perPathSeo[normalizedPath] || baseSeo;
var canonicalUrl = origin + normalizedPath;

document.title = selectedSeo.title
document.title = selectedSeo.title;

var descriptionTag = document.querySelector('meta[name="description"]')
if (descriptionTag) descriptionTag.setAttribute('content', selectedSeo.description)
var descriptionTag = document.querySelector('meta[name="description"]');
if (descriptionTag) descriptionTag.setAttribute("content", selectedSeo.description);

var canonicalTag = document.querySelector('link[data-seo="canonical"]')
if (canonicalTag) canonicalTag.setAttribute('href', canonicalUrl)
var canonicalTag = document.querySelector('link[data-seo="canonical"]');
if (canonicalTag) canonicalTag.setAttribute("href", canonicalUrl);

var ogUrlTag = document.querySelector('meta[data-seo="og:url"]')
if (ogUrlTag) ogUrlTag.setAttribute('content', canonicalUrl)
var ogUrlTag = document.querySelector('meta[data-seo="og:url"]');
if (ogUrlTag) ogUrlTag.setAttribute("content", canonicalUrl);

var ogTitleTag = document.querySelector('meta[data-seo="og:title"]')
if (ogTitleTag) ogTitleTag.setAttribute('content', selectedSeo.title)
var ogTitleTag = document.querySelector('meta[data-seo="og:title"]');
if (ogTitleTag) ogTitleTag.setAttribute("content", selectedSeo.title);

var ogDescriptionTag = document.querySelector('meta[data-seo="og:description"]')
if (ogDescriptionTag) ogDescriptionTag.setAttribute('content', selectedSeo.description)
var ogDescriptionTag = document.querySelector('meta[data-seo="og:description"]');
if (ogDescriptionTag) ogDescriptionTag.setAttribute("content", selectedSeo.description);

var ogImageTag = document.querySelector('meta[data-seo="og:image"]')
if (ogImageTag) ogImageTag.setAttribute('content', origin + selectedSeo.imagePath)
var ogImageTag = document.querySelector('meta[data-seo="og:image"]');
if (ogImageTag) ogImageTag.setAttribute("content", origin + selectedSeo.imagePath);

var twitterTitleTag = document.querySelector('meta[data-seo="twitter:title"]')
if (twitterTitleTag) twitterTitleTag.setAttribute('content', selectedSeo.title)
var twitterTitleTag = document.querySelector('meta[data-seo="twitter:title"]');
if (twitterTitleTag) twitterTitleTag.setAttribute("content", selectedSeo.title);

var twitterDescriptionTag = document.querySelector('meta[data-seo="twitter:description"]')
if (twitterDescriptionTag) twitterDescriptionTag.setAttribute('content', selectedSeo.description)
var twitterDescriptionTag = document.querySelector('meta[data-seo="twitter:description"]');
if (twitterDescriptionTag)
twitterDescriptionTag.setAttribute("content", selectedSeo.description);

var twitterImageTag = document.querySelector('meta[data-seo="twitter:image"]')
if (twitterImageTag) twitterImageTag.setAttribute('content', origin + selectedSeo.imagePath)
var twitterImageTag = document.querySelector('meta[data-seo="twitter:image"]');
if (twitterImageTag)
twitterImageTag.setAttribute("content", origin + selectedSeo.imagePath);

var structuredSchemaTag = document.getElementById('java-chat-structured-data')
var structuredSchemaTag = document.getElementById("java-chat-structured-data");
if (structuredSchemaTag) {
structuredSchemaTag.textContent = JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'Java Chat',
url: origin + '/',
applicationCategory: 'EducationalApplication',
operatingSystem: 'Web',
description: baseSeo.description
})
"@context": "https://schema.org",
"@type": "WebApplication",
name: "Java Chat",
url: origin + "/",
applicationCategory: "EducationalApplication",
operatingSystem: "Web",
description: baseSeo.description,
});
}
})()
})();
</script>
</head>
<body>
Expand Down
28 changes: 14 additions & 14 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
{
"name": "java-chat-frontend",
"private": true,
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": "22.17.0"
},
"scripts": {
"dev": "vite",
"build": "vite build",
Expand All @@ -23,6 +20,13 @@
"format:check": "oxfmt --check .",
"validate": "npm run format:check && npm run lint && npm run check"
},
"dependencies": {
"@types/dompurify": "^3.0.5",
"dompurify": "^3.3.1",
"highlight.js": "^11.10.0",
"marked": "^15.0.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@testing-library/jest-dom": "^6.9.1",
Expand All @@ -41,22 +45,18 @@
"vite": "^6.0.0",
"vitest": "^4.0.18"
},
"dependencies": {
"@types/dompurify": "^3.0.5",
"dompurify": "^3.3.1",
"highlight.js": "^11.10.0",
"marked": "^15.0.0",
"zod": "^3.25.76"
"overrides": {
"eslint-plugin-zod": {
"zod": "$zod"
}
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"oxfmt --write",
"oxlint -c config/oxlintrc.json --type-aware"
]
},
"overrides": {
"eslint-plugin-zod": {
"zod": "$zod"
}
"engines": {
"node": "22.17.0"
}
}
Loading
Loading