separated scripts inside config file and fixed nav bar

This commit is contained in:
Ramon Perez 2025-09-05 21:33:59 +10:00
parent 9e8dc9f84a
commit aea474bf57
5 changed files with 233 additions and 160 deletions

View File

@ -30,151 +30,11 @@ export default defineConfig({
head: [ head: [
{ {
tag: 'script', tag: 'script',
content: ` attrs: { src: '/scripts/inject-navigation.js', defer: true },
// Navigation configuration for Jan docs
const JAN_NAV_CONFIG = {
// Product navigation links - easy to extend for multiple products
links: [
{
href: '/',
text: 'Docs',
isActive: (path) => path === '/' || (path.startsWith('/') && !path.startsWith('/api'))
}, },
{ {
href: '/api', tag: 'link',
text: 'API Reference', attrs: { rel: 'stylesheet', href: '/styles/navigation.css' },
isActive: (path) => path.startsWith('/api')
}
],
// Pages that have their own navigation (don't inject nav)
excludePaths: ['/api-reference/', '/api/']
};
document.addEventListener('DOMContentLoaded', function() {
// Update logo link
const logoLink = document.querySelector('a[href="/"]');
if (logoLink && logoLink.getAttribute('href') === '/') {
logoLink.href = 'https://jan.ai';
}
// Add navigation to regular docs pages
// Add navigation to docs pages with retry logic
function addNavigation(retries = 0) {
const currentPath = window.location.pathname;
// Skip if page has its own navigation
const shouldSkipNav = JAN_NAV_CONFIG.excludePaths.some(
path => currentPath.startsWith(path)
);
if (shouldSkipNav) return;
const header = document.querySelector('.header');
const siteTitle = document.querySelector('.site-title');
if (header && siteTitle && !document.querySelector('.custom-nav-links')) {
// Find the right container for nav links
const searchContainer = header.querySelector('[class*="search"]')?.parentElement;
const targetContainer = searchContainer || header.querySelector('.sl-flex') || header;
if (targetContainer) {
// Create navigation container
const nav = document.createElement('nav');
nav.className = 'custom-nav-links';
nav.setAttribute('aria-label', 'Product Navigation');
// Create links from configuration
JAN_NAV_CONFIG.links.forEach(link => {
const a = document.createElement('a');
a.href = link.href;
a.textContent = link.text;
a.className = 'nav-link';
// Set active state
if (link.isActive(currentPath)) {
a.setAttribute('aria-current', 'page');
}
nav.appendChild(a);
});
// Insert navigation in the optimal position
if (searchContainer) {
targetContainer.insertBefore(nav, searchContainer);
} else {
targetContainer.appendChild(nav);
}
} else if (retries < 3) {
// Retry if container not found yet
setTimeout(() => addNavigation(retries + 1), 200);
}
} else if (retries < 3) {
// Retry if header/title not found yet
setTimeout(() => addNavigation(retries + 1), 200);
}
}
// Start navigation injection
if (document.readyState === 'loading') {
setTimeout(() => addNavigation(), 100);
} else {
addNavigation();
}
});
`,
},
{
tag: 'style',
content: `
/* Navigation links for regular docs pages */
.custom-nav-links {
display: inline-flex;
align-items: center;
gap: 0.5rem;
margin: 0 1rem;
}
.custom-nav-links .nav-link {
display: inline-flex;
align-items: center;
padding: 0.5rem 0.875rem;
border-radius: 0.375rem;
color: var(--sl-color-gray-2);
text-decoration: none;
font-weight: 500;
font-size: 0.875rem;
transition: all 0.2s ease;
white-space: nowrap;
}
.custom-nav-links .nav-link:hover {
color: var(--sl-color-text);
background: var(--sl-color-gray-6);
}
.custom-nav-links .nav-link[aria-current="page"] {
color: var(--sl-color-text);
background: var(--sl-color-gray-6);
}
/* Responsive design */
@media (max-width: 768px) {
.custom-nav-links {
display: none;
}
}
@media (min-width: 768px) and (max-width: 1024px) {
.custom-nav-links {
margin: 0 0.5rem;
}
.custom-nav-links .nav-link {
padding: 0.375rem 0.625rem;
font-size: 0.8125rem;
}
}
`,
}, },
], ],

View File

@ -1,7 +1,7 @@
{ {
"openapi": "3.1.0", "openapi": "3.1.0",
"info": { "info": {
"title": "👋Jan API", "title": "Jan API",
"description": "OpenAI-compatible API for local inference with Jan. Run AI models locally with complete privacy using llama.cpp's high-performance inference engine. Supports GGUF models with CPU and GPU acceleration. No authentication required for local usage.", "description": "OpenAI-compatible API for local inference with Jan. Run AI models locally with complete privacy using llama.cpp's high-performance inference engine. Supports GGUF models with CPU and GPU acceleration. No authentication required for local usage.",
"version": "0.3.14", "version": "0.3.14",
"contact": { "contact": {
@ -49,7 +49,9 @@
"paths": { "paths": {
"/v1/completions": { "/v1/completions": {
"post": { "post": {
"tags": ["Completions"], "tags": [
"Completions"
],
"summary": "Create completion", "summary": "Create completion",
"description": "Creates a completion for the provided prompt and parameters. This endpoint is compatible with OpenAI's completions API.", "description": "Creates a completion for the provided prompt and parameters. This endpoint is compatible with OpenAI's completions API.",
"operationId": "create_completion", "operationId": "create_completion",
@ -90,7 +92,11 @@
"prompt": "# Python function to calculate fibonacci\ndef fibonacci(n):", "prompt": "# Python function to calculate fibonacci\ndef fibonacci(n):",
"max_tokens": 200, "max_tokens": 200,
"temperature": 0.3, "temperature": 0.3,
"stop": ["\n\n", "def ", "class "] "stop": [
"\n\n",
"def ",
"class "
]
} }
}, },
"streaming": { "streaming": {
@ -151,7 +157,9 @@
}, },
"/v1/chat/completions": { "/v1/chat/completions": {
"post": { "post": {
"tags": ["Chat"], "tags": [
"Chat"
],
"summary": "Create chat completion", "summary": "Create chat completion",
"description": "Creates a model response for the given chat conversation. This endpoint is compatible with OpenAI's chat completions API.", "description": "Creates a model response for the given chat conversation. This endpoint is compatible with OpenAI's chat completions API.",
"operationId": "create_chat_completion", "operationId": "create_chat_completion",
@ -311,7 +319,9 @@
}, },
"/v1/models": { "/v1/models": {
"get": { "get": {
"tags": ["Models"], "tags": [
"Models"
],
"summary": "List available models", "summary": "List available models",
"description": "Lists the currently available models and provides basic information about each one such as the owner and availability.", "description": "Lists the currently available models and provides basic information about each one such as the owner and availability.",
"operationId": "list_models", "operationId": "list_models",
@ -360,7 +370,9 @@
}, },
"/extras/tokenize": { "/extras/tokenize": {
"post": { "post": {
"tags": ["Extras"], "tags": [
"Extras"
],
"summary": "Tokenize text", "summary": "Tokenize text",
"description": "Convert text input into tokens using the model's tokenizer.", "description": "Convert text input into tokens using the model's tokenizer.",
"operationId": "tokenize", "operationId": "tokenize",
@ -387,7 +399,12 @@
"$ref": "#/components/schemas/TokenizeResponse" "$ref": "#/components/schemas/TokenizeResponse"
}, },
"example": { "example": {
"tokens": [15339, 11, 1917, 0] "tokens": [
15339,
11,
1917,
0
]
} }
} }
} }
@ -397,7 +414,9 @@
}, },
"/extras/tokenize/count": { "/extras/tokenize/count": {
"post": { "post": {
"tags": ["Extras"], "tags": [
"Extras"
],
"summary": "Count tokens", "summary": "Count tokens",
"description": "Count the number of tokens in the provided text.", "description": "Count the number of tokens in the provided text.",
"operationId": "count_tokens", "operationId": "count_tokens",
@ -453,7 +472,9 @@
] ]
} }
}, },
"required": ["input"] "required": [
"input"
]
}, },
"TokenizeResponse": { "TokenizeResponse": {
"type": "object", "type": "object",
@ -466,7 +487,9 @@
"description": "Array of token IDs" "description": "Array of token IDs"
} }
}, },
"required": ["tokens"] "required": [
"tokens"
]
}, },
"TokenCountResponse": { "TokenCountResponse": {
"type": "object", "type": "object",
@ -476,7 +499,9 @@
"description": "Number of tokens" "description": "Number of tokens"
} }
}, },
"required": ["count"] "required": [
"count"
]
} }
}, },
"securitySchemes": { "securitySchemes": {
@ -505,7 +530,10 @@
"no_telemetry": true, "no_telemetry": true,
"offline_capable": true "offline_capable": true
}, },
"model_formats": ["GGUF", "GGML"], "model_formats": [
"GGUF",
"GGML"
],
"default_settings": { "default_settings": {
"context_length": 4096, "context_length": 4096,
"batch_size": 512, "batch_size": 512,

View File

@ -0,0 +1,119 @@
// Navigation injection script for Jan documentation
// This script adds navigation links to regular docs pages (not API reference pages)
;(function () {
// Navigation configuration for Jan docs
const JAN_NAV_CONFIG = {
// Product navigation links - easy to extend for multiple products
links: [
{
href: '/',
text: 'Docs',
isActive: (path) =>
path === '/' || (path.startsWith('/') && !path.startsWith('/api')),
},
{
href: '/api',
text: 'API Reference',
isActive: (path) => path.startsWith('/api'),
},
],
// Pages that have their own navigation (don't inject nav)
excludePaths: ['/api-reference/', '/api/'],
}
// Add navigation to docs pages with retry logic
function addNavigation(retries = 0) {
const currentPath = window.location.pathname
// Skip if page has its own navigation
const shouldSkipNav = JAN_NAV_CONFIG.excludePaths.some((path) =>
currentPath.startsWith(path)
)
if (shouldSkipNav) return
const header = document.querySelector('.header')
const siteTitle = document.querySelector('.site-title')
const existingNav = document.querySelector('.custom-nav-links')
if (header && siteTitle && !existingNav) {
// Find the right container for nav links
const searchElement = header.querySelector('[class*="search"]')
const flexContainer = header.querySelector('.sl-flex')
const targetContainer = flexContainer || header
if (targetContainer) {
// Create navigation container
const nav = document.createElement('nav')
nav.className = 'custom-nav-links'
nav.setAttribute('aria-label', 'Product Navigation')
// Create links from configuration
JAN_NAV_CONFIG.links.forEach((link) => {
const a = document.createElement('a')
a.href = link.href
a.textContent = link.text
a.className = 'nav-link'
// Set active state
if (link.isActive(currentPath)) {
a.setAttribute('aria-current', 'page')
}
nav.appendChild(a)
})
// Insert navigation safely
if (searchElement && targetContainer.contains(searchElement)) {
targetContainer.insertBefore(nav, searchElement)
} else {
// Find site title and insert after it
if (siteTitle && targetContainer.contains(siteTitle)) {
siteTitle.insertAdjacentElement('afterend', nav)
} else {
targetContainer.appendChild(nav)
}
}
} else if (retries < 5) {
setTimeout(() => addNavigation(retries + 1), 500)
}
} else if (retries < 5) {
setTimeout(() => addNavigation(retries + 1), 500)
}
}
// Initialize navigation injection
function initNavigation() {
// Update logo link to jan.ai
const logoLink = document.querySelector('a[href="/"]')
if (logoLink && logoLink.getAttribute('href') === '/') {
logoLink.href = 'https://jan.ai'
}
// Start navigation injection
if (document.readyState === 'loading') {
setTimeout(() => addNavigation(), 1000)
} else {
addNavigation()
}
}
// Run when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initNavigation)
} else {
initNavigation()
}
// Handle page navigation in SPA-like environments
let lastUrl = location.href
new MutationObserver(() => {
const url = location.href
if (url !== lastUrl) {
lastUrl = url
// Re-run navigation injection after navigation
setTimeout(() => addNavigation(), 100)
}
}).observe(document, { subtree: true, childList: true })
})()

View File

@ -0,0 +1,48 @@
/* Navigation links for regular docs pages */
.custom-nav-links {
display: inline-flex;
align-items: center;
gap: 0.5rem;
margin: 0 1rem;
}
.custom-nav-links .nav-link {
display: inline-flex;
align-items: center;
padding: 0.5rem 0.875rem;
border-radius: 0.375rem;
color: var(--sl-color-gray-2);
text-decoration: none;
font-weight: 500;
font-size: 0.875rem;
transition: all 0.2s ease;
white-space: nowrap;
}
.custom-nav-links .nav-link:hover {
color: var(--sl-color-text);
background: var(--sl-color-gray-6);
}
.custom-nav-links .nav-link[aria-current="page"] {
color: var(--sl-color-text);
background: var(--sl-color-gray-6);
}
/* Responsive design */
@media (max-width: 768px) {
.custom-nav-links {
display: none;
}
}
@media (min-width: 768px) and (max-width: 1024px) {
.custom-nav-links {
margin: 0 0.5rem;
}
.custom-nav-links .nav-link {
padding: 0.375rem 0.625rem;
font-size: 0.8125rem;
}
}

View File

@ -1,4 +1,22 @@
--- <!DOCTYPE html>
// Redirect to the new API documentation landing page <html lang="en">
return Astro.redirect('/api', 301); <head>
--- <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Redirecting to API Documentation | Jan</title>
<meta http-equiv="refresh" content="0; url=/api" />
<link rel="canonical" href="/api" />
</head>
<body>
<div style="display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: system-ui, sans-serif;">
<div style="text-align: center;">
<h1>Redirecting...</h1>
<p>If you are not redirected automatically, <a href="/api">click here to go to the API Documentation</a>.</p>
</div>
</div>
<script>
// Fallback redirect in case meta refresh doesn't work
window.location.href = '/api';
</script>
</body>
</html>