This commit is contained in:
Flatlogic Bot 2026-05-24 17:47:22 +00:00
parent afdc19bebf
commit 67d9fa33e5
5 changed files with 1052 additions and 146 deletions

View File

@ -6,7 +6,6 @@ const passport = require('passport');
const path = require('path');
const fs = require('fs');
const bodyParser = require('body-parser');
const db = require('./db/models');
const config = require('./config');
const swaggerUI = require('swagger-ui-express');
const swaggerJsDoc = require('swagger-jsdoc');
@ -16,6 +15,7 @@ const fileRoutes = require('./routes/file');
const searchRoutes = require('./routes/search');
const sqlRoutes = require('./routes/sql');
const pexelsRoutes = require('./routes/pexels');
const publicLlmRoutes = require('./routes/publicLlm');
const openaiRoutes = require('./routes/openai');
@ -101,7 +101,7 @@ app.use('/api/auth', authRoutes);
app.use('/api/file', fileRoutes);
app.use('/api/pexels', pexelsRoutes);
app.enable('trust proxy');
app.use('/api/public-llm', publicLlmRoutes);
app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes);

View File

@ -0,0 +1,343 @@
const express = require('express');
const wrapAsync = require('../helpers').wrapAsync;
const { LocalAIApi } = require('../ai/LocalAIApi');
const router = express.Router();
const DIAGNOSTICS_CACHE_TTL_MS = 2 * 60 * 1000;
const DEFAULT_PROBE_PROMPT = 'Reply with OK only.';
let diagnosticsCache = {
expiresAt: 0,
payload: null,
};
const RELEVANT_ENV_KEYS = [
'AI_PROXY_BASE_URL',
'AI_RESPONSES_PATH',
'AI_DEFAULT_MODEL',
'AI_PROJECT_HEADER',
'AI_TIMEOUT',
'AI_VERIFY_TLS',
'PROJECT_ID',
'PROJECT_UUID',
'OPENAI_API_KEY',
'GEMINI_API_KEY',
'GEMINI_MODEL',
'GOOGLE_API_KEY',
'GOOGLE_CLOUD_PROJECT',
'GOOGLE_GENAI_USE_VERTEXAI',
'OPENCODE_MODEL',
];
function shellQuote(value) {
return `'${String(value ?? '').replace(/'/g, `'"'"'`)}'`;
}
function uniqueStrings(values) {
return [...new Set((values || [])
.map((value) => String(value || '').trim())
.filter(Boolean))];
}
function getEnvironmentEntries() {
return Object.keys(process.env)
.sort((left, right) => left.localeCompare(right))
.map((key) => ({
key,
value: String(process.env[key] ?? ''),
}));
}
function getRelevantEnvironmentEntries() {
const availableKeys = uniqueStrings([
...RELEVANT_ENV_KEYS,
...Object.keys(process.env).filter((key) => /(AI|LLM|MODEL|OPENAI|GEMINI|GOOGLE|PROJECT|API)/i.test(key)),
]);
return availableKeys
.filter((key) => Object.prototype.hasOwnProperty.call(process.env, key))
.sort((left, right) => left.localeCompare(right))
.map((key) => ({
key,
value: String(process.env[key] ?? ''),
}));
}
function buildAbsoluteUrl(baseUrl, pathValue) {
const trimmedBase = String(baseUrl || '').replace(/\/+$/, '');
const trimmedPath = String(pathValue || '').trim();
if (!trimmedPath) {
return trimmedBase;
}
if (/^https?:\/\//i.test(trimmedPath)) {
return trimmedPath;
}
if (trimmedPath.startsWith('/')) {
return `${trimmedBase}${trimmedPath}`;
}
return `${trimmedBase}/${trimmedPath}`;
}
function resolveProxyConfig() {
const baseUrl = process.env.AI_PROXY_BASE_URL || 'https://flatlogic.com';
const projectId = process.env.PROJECT_ID || null;
let responsesPath = process.env.AI_RESPONSES_PATH || null;
if (!responsesPath && projectId) {
responsesPath = `/projects/${projectId}/ai-request`;
}
const defaultModel = process.env.AI_DEFAULT_MODEL || 'gpt-5-mini';
const projectHeader = process.env.AI_PROJECT_HEADER || 'project-uuid';
const projectUuid = process.env.PROJECT_UUID || null;
return {
baseUrl,
responsesPath,
resolvedUrl: buildAbsoluteUrl(baseUrl, responsesPath),
defaultModel,
projectHeader,
projectUuid,
projectId,
timeoutSeconds: Number.parseInt(String(process.env.AI_TIMEOUT || 30), 10) || 30,
verifyTls: !['false', '0'].includes(String(process.env.AI_VERIFY_TLS ?? 'true').toLowerCase()),
};
}
function buildRequestOrigin(req) {
return `${req.protocol}://${req.get('host')}`;
}
function buildProbeCandidates(proxyConfig) {
return uniqueStrings([
proxyConfig.defaultModel,
'gpt-5-mini',
'gpt-4.1-mini',
'gpt-4o-mini',
process.env.GEMINI_MODEL,
process.env.OPENCODE_MODEL,
]);
}
async function probeModel(model) {
const startedAt = Date.now();
try {
const response = await LocalAIApi.createResponse(
{
model,
input: [{ role: 'user', content: DEFAULT_PROBE_PROMPT }],
},
{
poll_interval: 2,
poll_timeout: 25,
},
);
if (response.success) {
return {
model,
success: true,
latencyMs: Date.now() - startedAt,
previewText: LocalAIApi.extractText(response) || null,
error: null,
};
}
return {
model,
success: false,
latencyMs: Date.now() - startedAt,
previewText: null,
error: response.error || response.message || 'unknown_error',
};
} catch (error) {
console.error(`Model probe failed for ${model}:`, error);
return {
model,
success: false,
latencyMs: Date.now() - startedAt,
previewText: null,
error: error.message || String(error),
};
}
}
function buildCurlPayload(model, prompt) {
return {
model,
input: [{ role: 'user', content: prompt }],
};
}
function joinCurlLines(lines) {
return lines
.filter(Boolean)
.map((line, index, allLines) => (index < allLines.length - 1 ? `${line} \\` : line))
.join('\n');
}
function buildAppProxyCurl(appProxyUrl, model, prompt) {
const payload = {
model,
prompt,
};
return joinCurlLines([
`curl -sS ${shellQuote(appProxyUrl)}`,
'-X POST',
"-H 'Content-Type: application/json'",
`--data ${shellQuote(JSON.stringify(payload))}`,
]);
}
function buildFlatlogicProxyCurl(proxyConfig, model, prompt) {
const payload = buildCurlPayload(model, prompt);
payload.project_uuid = proxyConfig.projectUuid;
const projectHeaderLine = shellQuote(`${proxyConfig.projectHeader}: ${proxyConfig.projectUuid || ''}`);
return joinCurlLines([
`curl -sS ${shellQuote(proxyConfig.resolvedUrl)}`,
'-X POST',
"-H 'Content-Type: application/json'",
`-H ${projectHeaderLine}`,
`--data ${shellQuote(JSON.stringify(payload))}`,
]);
}
function buildOpenAICurl(model, prompt) {
const apiKey = process.env.OPENAI_API_KEY || '';
const payload = buildCurlPayload(model, prompt);
const url = 'https://api.openai.com/v1/responses';
const authorizationHeader = shellQuote(`Authorization: Bearer ${apiKey}`);
return joinCurlLines([
`curl -sS ${shellQuote(url)}`,
'-X POST',
"-H 'Content-Type: application/json'",
`-H ${authorizationHeader}`,
`--data ${shellQuote(JSON.stringify(payload))}`,
]);
}
function buildGeminiCurl(prompt) {
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || '';
const configuredModel = String(process.env.GEMINI_MODEL || '').trim();
const model = configuredModel.startsWith('gemini-') ? configuredModel : 'gemini-2.5-flash';
const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(apiKey)}`;
const payload = {
contents: [
{
role: 'user',
parts: [{ text: prompt }],
},
],
};
return joinCurlLines([
`curl -sS ${shellQuote(url)}`,
'-X POST',
"-H 'Content-Type: application/json'",
`--data ${shellQuote(JSON.stringify(payload))}`,
]);
}
async function buildDiagnostics(req) {
const proxyConfig = resolveProxyConfig();
const requestOrigin = buildRequestOrigin(req);
const appProxyUrl = `${requestOrigin}/api/public-llm/proxy`;
const defaultPrompt = 'Reply with a concise health check for this LLM proxy.';
const probeCandidates = buildProbeCandidates(proxyConfig);
const probeResults = await Promise.all(probeCandidates.map((model) => probeModel(model)));
const availableModels = probeResults.filter((result) => result.success).map((result) => result.model);
const unavailableModels = probeResults.filter((result) => !result.success).map((result) => result.model);
const preferredModel = availableModels[0] || probeCandidates[0] || proxyConfig.defaultModel;
return {
generatedAt: new Date().toISOString(),
requestOrigin,
appProxyUrl,
defaultPrompt,
envVars: getEnvironmentEntries(),
llmEnvVars: getRelevantEnvironmentEntries(),
proxyConfig,
probeCandidates,
probeResults,
availableModels,
unavailableModels,
curlCommands: {
appProxy: buildAppProxyCurl(appProxyUrl, preferredModel, defaultPrompt),
flatlogicProxy: buildFlatlogicProxyCurl(proxyConfig, preferredModel, defaultPrompt),
openaiDirect: buildOpenAICurl(preferredModel, defaultPrompt),
geminiDirect: buildGeminiCurl(defaultPrompt),
},
};
}
router.get(
'/diagnostics',
wrapAsync(async (req, res) => {
const forceRefresh = String(req.query.refresh || '').toLowerCase() === 'true';
if (!forceRefresh && diagnosticsCache.payload && diagnosticsCache.expiresAt > Date.now()) {
return res.status(200).send(diagnosticsCache.payload);
}
const payload = await buildDiagnostics(req);
diagnosticsCache = {
expiresAt: Date.now() + DIAGNOSTICS_CACHE_TTL_MS,
payload,
};
return res.status(200).send(payload);
}),
);
router.post(
'/proxy',
wrapAsync(async (req, res) => {
const prompt = typeof req.body.prompt === 'string' ? req.body.prompt.trim() : '';
const input = Array.isArray(req.body.input) ? req.body.input : null;
const model = typeof req.body.model === 'string' && req.body.model.trim()
? req.body.model.trim()
: resolveProxyConfig().defaultModel;
if (!prompt && !input) {
return res.status(400).send({
success: false,
error: 'input_missing',
message: 'Either "prompt" or "input" must be provided.',
});
}
const payload = {
model,
input: input || [{ role: 'user', content: prompt }],
};
const response = await LocalAIApi.createResponse(payload, {
poll_interval: 2,
poll_timeout: 60,
});
if (!response.success) {
console.error('Public LLM proxy error:', response);
const status = response.error === 'input_missing' ? 400 : 502;
return res.status(status).send(response);
}
return res.status(200).send({
success: true,
model,
text: LocalAIApi.extractText(response) || '',
response,
});
}),
);
module.exports = router;

View File

@ -1,6 +1,5 @@
import React, {useEffect, useRef} from 'react'
import React, { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'

View File

@ -1,5 +1,4 @@
import React, { ReactNode, useEffect } from 'react'
import { useState } from 'react'
import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'

View File

@ -1,166 +1,731 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import axios from 'axios';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import NotificationBar from '../components/NotificationBar';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
import LayoutGuest from '../layouts/Guest';
type EnvironmentEntry = {
key: string;
value: string;
};
type ProxyConfig = {
baseUrl: string;
responsesPath: string | null;
resolvedUrl: string;
defaultModel: string;
projectHeader: string;
projectUuid: string | null;
projectId: string | null;
timeoutSeconds: number;
verifyTls: boolean;
};
type ProbeResult = {
model: string;
success: boolean;
latencyMs: number;
previewText: string | null;
error: string | null;
};
type CurlCommands = {
appProxy: string;
flatlogicProxy: string;
openaiDirect: string;
geminiDirect: string;
};
type DiagnosticsPayload = {
generatedAt: string;
requestOrigin: string;
appProxyUrl: string;
defaultPrompt: string;
envVars: EnvironmentEntry[];
llmEnvVars: EnvironmentEntry[];
proxyConfig: ProxyConfig;
probeCandidates: string[];
probeResults: ProbeResult[];
availableModels: string[];
unavailableModels: string[];
curlCommands: CurlCommands;
};
type ProxyResponsePayload = {
success: boolean;
model: string;
text: string;
response: Record<string, unknown>;
};
type FeedbackState = {
type: 'success' | 'error';
text: string;
} | null;
const shellQuote = (value: string) => `'${String(value ?? '').replace(/'/g, `'"'"'`)}'`;
const joinCurlLines = (lines: string[]) => lines
.filter(Boolean)
.map((line, index, allLines) => (index < allLines.length - 1 ? `${line} \\` : line))
.join('\n');
const buildResponsePayload = (model: string, prompt: string) => ({
model,
input: [{ role: 'user', content: prompt }],
});
const buildGeminiPayload = (prompt: string) => ({
contents: [
{
role: 'user',
parts: [{ text: prompt }],
},
],
});
const formatDateTime = (value?: string) => {
if (!value) {
return '—';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
return date.toLocaleString();
};
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('video');
const [contentPosition, setContentPosition] = useState('left');
const textColor = useAppSelector((state) => state.style.linkColor);
const [diagnostics, setDiagnostics] = useState<DiagnosticsPayload | null>(null);
const [isLoadingDiagnostics, setIsLoadingDiagnostics] = useState(true);
const [diagnosticsError, setDiagnosticsError] = useState('');
const [prompt, setPrompt] = useState('Reply with a concise health check for this LLM proxy.');
const [selectedModel, setSelectedModel] = useState('');
const [envSearch, setEnvSearch] = useState('');
const [proxyResponse, setProxyResponse] = useState<ProxyResponsePayload | null>(null);
const [proxyError, setProxyError] = useState('');
const [isProxying, setIsProxying] = useState(false);
const [feedback, setFeedback] = useState<FeedbackState>(null);
const title = 'App Preview'
const loadDiagnostics = async (forceRefresh = false) => {
setIsLoadingDiagnostics(true);
setDiagnosticsError('');
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
try {
const response = await axios.get<DiagnosticsPayload>(
forceRefresh ? '/public-llm/diagnostics?refresh=true' : '/public-llm/diagnostics',
);
const payload = response.data;
setDiagnostics(payload);
setPrompt((currentPrompt) => currentPrompt || payload.defaultPrompt);
setSelectedModel((currentModel) => currentModel || payload.availableModels[0] || payload.proxyConfig.defaultModel);
} catch (error) {
console.error('Failed to load diagnostics:', error);
setDiagnosticsError(error instanceof Error ? error.message : 'Failed to load diagnostics.');
} finally {
setIsLoadingDiagnostics(false);
}
};
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
useEffect(() => {
loadDiagnostics();
}, []);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
const environmentMap = useMemo(() => {
const entries = diagnostics?.envVars || [];
return new Map(entries.map((entry) => [entry.key, entry.value]));
}, [diagnostics]);
const activeModel = useMemo(() => {
if (!diagnostics) {
return selectedModel || 'gpt-5-mini';
}
return selectedModel.trim() || diagnostics.availableModels[0] || diagnostics.proxyConfig.defaultModel;
}, [diagnostics, selectedModel]);
const activePrompt = useMemo(() => {
return prompt.trim() || diagnostics?.defaultPrompt || 'Reply with OK only.';
}, [diagnostics?.defaultPrompt, prompt]);
const filteredEnvVars = useMemo(() => {
const query = envSearch.trim().toLowerCase();
const entries = diagnostics?.envVars || [];
if (!query) {
return entries;
}
return entries.filter((entry) => {
const haystack = `${entry.key} ${entry.value}`.toLowerCase();
return haystack.includes(query);
});
}, [diagnostics?.envVars, envSearch]);
const curlCards = useMemo(() => {
if (!diagnostics) {
return [];
}
const openAiKey = environmentMap.get('OPENAI_API_KEY') || '';
const geminiKey = environmentMap.get('GEMINI_API_KEY') || environmentMap.get('GOOGLE_API_KEY') || '';
const geminiModelFromEnv = (environmentMap.get('GEMINI_MODEL') || '').trim();
const geminiModel = geminiModelFromEnv.startsWith('gemini-') ? geminiModelFromEnv : 'gemini-2.5-flash';
const appProxyCommand = joinCurlLines([
`curl -sS ${shellQuote(diagnostics.appProxyUrl)}`,
'-X POST',
"-H 'Content-Type: application/json'",
`--data ${shellQuote(JSON.stringify({ model: activeModel, prompt: activePrompt }))}`,
]);
const flatlogicProxyCommand = joinCurlLines([
`curl -sS ${shellQuote(diagnostics.proxyConfig.resolvedUrl)}`,
'-X POST',
"-H 'Content-Type: application/json'",
`-H ${shellQuote(`${diagnostics.proxyConfig.projectHeader}: ${diagnostics.proxyConfig.projectUuid || ''}`)}`,
`--data ${shellQuote(JSON.stringify({
...buildResponsePayload(activeModel, activePrompt),
project_uuid: diagnostics.proxyConfig.projectUuid,
}))}`,
]);
const openAiCommand = joinCurlLines([
`curl -sS ${shellQuote('https://api.openai.com/v1/responses')}`,
'-X POST',
"-H 'Content-Type: application/json'",
`-H ${shellQuote(`Authorization: Bearer ${openAiKey}`)}`,
`--data ${shellQuote(JSON.stringify(buildResponsePayload(activeModel, activePrompt)))}`,
]);
const geminiCommand = joinCurlLines([
`curl -sS ${shellQuote(`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(geminiModel)}:generateContent?key=${encodeURIComponent(geminiKey)}`)}`,
'-X POST',
"-H 'Content-Type: application/json'",
`--data ${shellQuote(JSON.stringify(buildGeminiPayload(activePrompt)))}`,
]);
return [
{
id: 'appProxy',
title: 'App proxy curl',
description: 'Calls the new public reverse-proxy endpoint exposed by this app.',
command: appProxyCommand,
},
{
id: 'flatlogicProxy',
title: 'Flatlogic AI proxy curl',
description: 'Uses the project UUID header and the configured Flatlogic AI proxy URL.',
command: flatlogicProxyCommand,
},
{
id: 'openaiDirect',
title: 'OpenAI direct curl',
description: 'Prints the full OpenAI URL and API key from the current environment.',
command: openAiCommand,
},
{
id: 'geminiDirect',
title: 'Gemini direct curl',
description: 'Prints a Gemini generateContent command with the current API key.',
command: geminiCommand,
},
];
}, [activeModel, activePrompt, diagnostics, environmentMap]);
const handleCopy = async (value: string, label: string) => {
try {
await navigator.clipboard.writeText(value);
setFeedback({ type: 'success', text: `${label} copied.` });
window.setTimeout(() => {
setFeedback((current) => (current?.text === `${label} copied.` ? null : current));
}, 2200);
} catch (error) {
console.error(`Failed to copy ${label}:`, error);
setFeedback({ type: 'error', text: `Failed to copy ${label}.` });
}
};
const handleProxySubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setProxyError('');
setProxyResponse(null);
setIsProxying(true);
try {
const response = await axios.post<ProxyResponsePayload>('/public-llm/proxy', {
model: activeModel,
prompt: activePrompt,
});
setProxyResponse(response.data);
} catch (error) {
console.error('Proxy request failed:', error);
setProxyError(error instanceof Error ? error.message : 'Proxy request failed.');
} finally {
setIsProxying(false);
}
};
const envExportText = useMemo(() => {
return (diagnostics?.envVars || [])
.map((entry) => `${entry.key}=${entry.value}`)
.join('\n');
}, [diagnostics?.envVars]);
const stats = [
{
label: 'Visible env vars',
value: diagnostics?.envVars.length ?? '—',
},
{
label: 'LLM env keys',
value: diagnostics?.llmEnvVars.length ?? '—',
},
{
label: 'Available models',
value: diagnostics?.availableModels.length ?? '—',
},
{
label: 'Failed probes',
value: diagnostics?.unavailableModels.length ?? '—',
},
];
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<>
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('LLM Proxy Console')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your App Preview app!"/>
<div className="space-y-3">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
<div className="min-h-screen bg-slate-950 text-slate-50">
<div className="relative isolate overflow-hidden bg-[radial-gradient(circle_at_top_left,_rgba(37,99,235,0.35),_transparent_32%),radial-gradient(circle_at_top_right,_rgba(14,165,233,0.18),_transparent_26%),linear-gradient(135deg,_#020617_0%,_#0f172a_55%,_#111827_100%)]">
<div className="absolute left-8 top-24 h-40 w-40 rounded-full bg-cyan-400/20 blur-3xl" />
<div className="absolute right-0 top-0 h-64 w-64 rounded-full bg-blue-500/20 blur-3xl" />
</BaseButtons>
</CardBox>
<main className="relative mx-auto max-w-7xl px-6 py-8 lg:px-8">
<header className="mb-8 flex flex-col gap-4 border-b border-white/10 pb-6 md:flex-row md:items-center md:justify-between">
<div>
<p className="mb-3 inline-flex items-center rounded-full border border-cyan-400/25 bg-cyan-400/10 px-3 py-1 text-xs font-medium uppercase tracking-[0.22em] text-cyan-200">
Public LLM reverse proxy diagnostics
</p>
<h1 className="max-w-4xl text-4xl font-semibold tracking-tight text-white md:text-5xl">
LLM Proxy Console with live probes, full env dump, and copy-ready curl commands.
</h1>
<p className="mt-4 max-w-3xl text-base leading-7 text-slate-300 md:text-lg">
LLM prompt
</p>
</div>
<div className="flex flex-wrap gap-3">
<BaseButton href="/login" label="Login" color="info" />
<BaseButton href="/dashboard" label="Admin interface" color="whiteDark" outline />
</div>
</header>
{feedback && (
<NotificationBar color={feedback.type === 'success' ? 'success' : 'danger'}>
{feedback.text}
</NotificationBar>
)}
{diagnosticsError && (
<NotificationBar color="danger">
Diagnostics failed to load: {diagnosticsError}
</NotificationBar>
)}
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{stats.map((stat) => (
<div
key={stat.label}
className="rounded-3xl border border-white/10 bg-white/5 p-5 shadow-[0_20px_50px_rgba(2,6,23,0.35)] backdrop-blur"
>
<div className="text-sm text-slate-400">{stat.label}</div>
<div className="mt-3 text-3xl font-semibold text-white">{stat.value}</div>
</div>
))}
</section>
<section className="mt-8 grid gap-6 xl:grid-cols-[1.2fr,0.8fr]">
<CardBox className="border border-white/10 bg-white/5 shadow-[0_24px_70px_rgba(2,6,23,0.38)] backdrop-blur">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<p className="text-sm font-medium uppercase tracking-[0.2em] text-cyan-200">Probe summary</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Model availability and proxy wiring</h2>
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-300">
These probes are executed against the configured Flatlogic AI proxy using live project credentials. Refresh to rerun the checks.
</p>
</div>
<div className="flex flex-wrap gap-3">
<button
type="button"
onClick={() => loadDiagnostics(true)}
className="rounded-xl border border-cyan-300/30 bg-cyan-300/10 px-4 py-2 text-sm font-medium text-cyan-100 transition hover:border-cyan-200/60 hover:bg-cyan-300/20 focus:outline-none focus:ring-4 focus:ring-cyan-400/20"
>
{isLoadingDiagnostics ? 'Refreshing…' : 'Refresh probes'}
</button>
<button
type="button"
onClick={() => handleCopy(JSON.stringify(diagnostics || {}, null, 2), 'Diagnostics JSON')}
className="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm font-medium text-slate-100 transition hover:border-white/20 hover:bg-white/10 focus:outline-none focus:ring-4 focus:ring-white/10"
>
Copy diagnostics JSON
</button>
</div>
</div>
<div className="mt-6 grid gap-6 lg:grid-cols-[1.1fr,0.9fr]">
<div className="space-y-4">
<div className="rounded-2xl border border-emerald-400/20 bg-emerald-400/10 p-4">
<div className="text-sm font-medium uppercase tracking-[0.18em] text-emerald-100">Available via proxy</div>
<div className="mt-3 flex flex-wrap gap-2">
{(diagnostics?.availableModels || []).length > 0 ? (
diagnostics?.availableModels.map((model) => (
<button
key={model}
type="button"
onClick={() => setSelectedModel(model)}
className={`rounded-full border px-3 py-1 text-sm transition ${
activeModel === model
? 'border-emerald-200 bg-emerald-200 text-slate-950'
: 'border-emerald-200/20 bg-emerald-200/10 text-emerald-50 hover:border-emerald-200/50'
}`}
>
{model}
</button>
))
) : (
<p className="text-sm text-emerald-50/80">No successful probes yet.</p>
)}
</div>
</div>
<div className="rounded-2xl border border-rose-300/15 bg-rose-300/10 p-4">
<div className="text-sm font-medium uppercase tracking-[0.18em] text-rose-100">Failed candidates</div>
<div className="mt-3 space-y-3">
{(diagnostics?.probeResults || []).filter((result) => !result.success).length > 0 ? (
diagnostics?.probeResults
.filter((result) => !result.success)
.map((result) => (
<div key={result.model} className="rounded-2xl border border-white/10 bg-slate-950/30 p-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="font-medium text-white">{result.model}</span>
<span className="text-xs uppercase tracking-[0.18em] text-rose-100/80">{result.latencyMs} ms</span>
</div>
<p className="mt-2 break-all text-sm text-slate-300">{result.error}</p>
</div>
))
) : (
<p className="text-sm text-rose-50/80">No failed probes in the latest run.</p>
)}
</div>
</div>
</div>
<div className="space-y-4">
<div className="rounded-2xl border border-white/10 bg-slate-950/35 p-4">
<div className="mb-3 text-sm font-medium uppercase tracking-[0.18em] text-slate-300">Proxy config</div>
<dl className="space-y-3 text-sm text-slate-200">
<div className="grid gap-1 sm:grid-cols-[140px,1fr]">
<dt className="text-slate-400">Base URL</dt>
<dd className="break-all font-mono text-xs text-slate-100">{diagnostics?.proxyConfig.baseUrl || '—'}</dd>
</div>
<div className="grid gap-1 sm:grid-cols-[140px,1fr]">
<dt className="text-slate-400">Response path</dt>
<dd className="break-all font-mono text-xs text-slate-100">{diagnostics?.proxyConfig.responsesPath || '—'}</dd>
</div>
<div className="grid gap-1 sm:grid-cols-[140px,1fr]">
<dt className="text-slate-400">Resolved URL</dt>
<dd className="break-all font-mono text-xs text-slate-100">{diagnostics?.proxyConfig.resolvedUrl || '—'}</dd>
</div>
<div className="grid gap-1 sm:grid-cols-[140px,1fr]">
<dt className="text-slate-400">Project header</dt>
<dd className="break-all font-mono text-xs text-slate-100">{diagnostics?.proxyConfig.projectHeader || '—'}</dd>
</div>
<div className="grid gap-1 sm:grid-cols-[140px,1fr]">
<dt className="text-slate-400">Project UUID</dt>
<dd className="break-all font-mono text-xs text-slate-100">{diagnostics?.proxyConfig.projectUuid || '—'}</dd>
</div>
<div className="grid gap-1 sm:grid-cols-[140px,1fr]">
<dt className="text-slate-400">Default model</dt>
<dd className="break-all font-mono text-xs text-slate-100">{diagnostics?.proxyConfig.defaultModel || '—'}</dd>
</div>
<div className="grid gap-1 sm:grid-cols-[140px,1fr]">
<dt className="text-slate-400">Refreshed</dt>
<dd className="text-xs text-slate-100">{formatDateTime(diagnostics?.generatedAt)}</dd>
</div>
</dl>
</div>
<div className="rounded-2xl border border-white/10 bg-slate-950/35 p-4">
<div className="mb-3 text-sm font-medium uppercase tracking-[0.18em] text-slate-300">Quick admin links</div>
<div className="flex flex-wrap gap-2 text-sm text-slate-200">
{[
{ href: '/dashboard', label: 'Dashboard' },
{ href: '/llm_requests/llm_requests-list', label: 'LLM requests' },
{ href: '/model_catalog/model_catalog-list', label: 'Model catalog' },
{ href: '/environment_variables/environment_variables-list', label: 'Environment variables' },
].map((item) => (
<Link
key={item.href}
href={item.href}
className="rounded-full border border-white/10 bg-white/5 px-3 py-1.5 transition hover:border-cyan-300/40 hover:bg-cyan-300/10"
>
{item.label}
</Link>
))}
</div>
</div>
</div>
</div>
</CardBox>
<CardBox className="border border-white/10 bg-white/5 shadow-[0_24px_70px_rgba(2,6,23,0.38)] backdrop-blur">
<div>
<p className="text-sm font-medium uppercase tracking-[0.2em] text-cyan-200">Reverse proxy test</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Send a live prompt through the app proxy</h2>
<p className="mt-2 text-sm leading-6 text-slate-300">
This uses <span className="font-mono text-cyan-100">/api/public-llm/proxy</span> and returns the extracted model text plus the raw upstream payload.
</p>
</div>
<form className="mt-6 space-y-4" onSubmit={handleProxySubmit}>
<div>
<label className="mb-2 block text-sm font-medium text-slate-200" htmlFor="model-input">
Model
</label>
<input
id="model-input"
value={selectedModel}
onChange={(event) => setSelectedModel(event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-slate-950/40 px-4 py-3 font-mono text-sm text-white outline-none transition placeholder:text-slate-500 focus:border-cyan-300/60 focus:ring-4 focus:ring-cyan-400/20"
placeholder="gpt-5-mini"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-slate-200" htmlFor="prompt-input">
Prompt
</label>
<textarea
id="prompt-input"
value={prompt}
onChange={(event) => setPrompt(event.target.value)}
rows={7}
className="w-full rounded-2xl border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-white outline-none transition placeholder:text-slate-500 focus:border-cyan-300/60 focus:ring-4 focus:ring-cyan-400/20"
placeholder="Ask the proxy anything…"
/>
</div>
<div className="flex flex-wrap gap-3">
<BaseButton type="submit" label={isProxying ? 'Sending…' : 'Run proxy test'} color="info" disabled={isProxying} />
<button
type="button"
onClick={() => {
setPrompt(diagnostics?.defaultPrompt || 'Reply with a concise health check for this LLM proxy.');
setSelectedModel(diagnostics?.availableModels[0] || diagnostics?.proxyConfig.defaultModel || 'gpt-5-mini');
setProxyError('');
setProxyResponse(null);
}}
className="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm font-medium text-slate-100 transition hover:border-white/20 hover:bg-white/10 focus:outline-none focus:ring-4 focus:ring-white/10"
>
Reset sample
</button>
</div>
</form>
{proxyError && (
<div className="mt-5 rounded-2xl border border-rose-300/15 bg-rose-300/10 p-4 text-sm text-rose-100">
{proxyError}
</div>
)}
{proxyResponse && (
<div className="mt-5 space-y-4">
<div className="rounded-2xl border border-emerald-300/15 bg-emerald-300/10 p-4">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium uppercase tracking-[0.18em] text-emerald-50">Extracted response</p>
<p className="mt-1 text-xs text-emerald-50/80">Model: {proxyResponse.model}</p>
</div>
<button
type="button"
onClick={() => handleCopy(proxyResponse.text, 'Proxy response text')}
className="rounded-xl border border-emerald-100/20 bg-emerald-100/10 px-3 py-1.5 text-xs font-medium text-emerald-50 transition hover:border-emerald-100/40 hover:bg-emerald-100/20"
>
Copy text
</button>
</div>
<pre className="mt-3 whitespace-pre-wrap break-words rounded-2xl bg-slate-950/40 p-4 text-sm text-white">{proxyResponse.text || '(empty text output)'}</pre>
</div>
<div className="rounded-2xl border border-white/10 bg-slate-950/35 p-4">
<div className="flex items-center justify-between gap-3">
<p className="text-sm font-medium uppercase tracking-[0.18em] text-slate-300">Raw upstream payload</p>
<button
type="button"
onClick={() => handleCopy(JSON.stringify(proxyResponse.response, null, 2), 'Raw proxy JSON')}
className="rounded-xl border border-white/10 bg-white/5 px-3 py-1.5 text-xs font-medium text-slate-100 transition hover:border-white/20 hover:bg-white/10"
>
Copy JSON
</button>
</div>
<pre className="mt-3 max-h-72 overflow-auto whitespace-pre-wrap break-words rounded-2xl bg-slate-950/60 p-4 text-xs text-slate-200">
{JSON.stringify(proxyResponse.response, null, 2)}
</pre>
</div>
</div>
)}
</CardBox>
</section>
<section className="mt-8 grid gap-6 xl:grid-cols-[0.9fr,1.1fr]">
<CardBox className="border border-white/10 bg-white/5 shadow-[0_24px_70px_rgba(2,6,23,0.38)] backdrop-blur">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<p className="text-sm font-medium uppercase tracking-[0.2em] text-cyan-200">LLM environment</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Relevant environment parameters discovered at runtime</h2>
</div>
<button
type="button"
onClick={() => handleCopy(JSON.stringify(diagnostics?.llmEnvVars || [], null, 2), 'LLM environment JSON')}
className="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm font-medium text-slate-100 transition hover:border-white/20 hover:bg-white/10 focus:outline-none focus:ring-4 focus:ring-white/10"
>
Copy LLM env JSON
</button>
</div>
<div className="mt-6 space-y-3">
{(diagnostics?.llmEnvVars || []).map((entry) => (
<div key={entry.key} className="rounded-2xl border border-white/10 bg-slate-950/35 p-4">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-cyan-200">{entry.key}</div>
<div className="mt-2 break-all font-mono text-xs leading-6 text-slate-200">{entry.value}</div>
</div>
<button
type="button"
onClick={() => handleCopy(entry.value, entry.key)}
className="rounded-xl border border-white/10 bg-white/5 px-3 py-1.5 text-xs font-medium text-slate-100 transition hover:border-white/20 hover:bg-white/10"
>
Copy
</button>
</div>
</div>
))}
</div>
</CardBox>
<CardBox className="border border-white/10 bg-white/5 shadow-[0_24px_70px_rgba(2,6,23,0.38)] backdrop-blur">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<p className="text-sm font-medium uppercase tracking-[0.2em] text-cyan-200">Curl commands</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Full commands with URL and key included</h2>
<p className="mt-2 text-sm leading-6 text-slate-300">
These commands are regenerated from the current model and prompt, so you can copy them straight into your terminal.
</p>
</div>
<button
type="button"
onClick={() => handleCopy(curlCards.map((item) => `# ${item.title}\n${item.command}`).join('\n\n'), 'All curl commands')}
className="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm font-medium text-slate-100 transition hover:border-white/20 hover:bg-white/10 focus:outline-none focus:ring-4 focus:ring-white/10"
>
Copy all curls
</button>
</div>
<div className="mt-6 grid gap-4">
{curlCards.map((item) => (
<div key={item.id} className="rounded-2xl border border-white/10 bg-slate-950/35 p-4">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<h3 className="text-lg font-semibold text-white">{item.title}</h3>
<p className="mt-1 max-w-2xl text-sm text-slate-300">{item.description}</p>
</div>
<button
type="button"
onClick={() => handleCopy(item.command, item.title)}
className="rounded-xl border border-cyan-300/30 bg-cyan-300/10 px-4 py-2 text-sm font-medium text-cyan-100 transition hover:border-cyan-200/60 hover:bg-cyan-300/20 focus:outline-none focus:ring-4 focus:ring-cyan-400/20"
>
Copy command
</button>
</div>
<pre className="mt-4 overflow-x-auto whitespace-pre-wrap break-words rounded-2xl bg-slate-950/70 p-4 text-xs leading-6 text-slate-100">{item.command}</pre>
</div>
))}
</div>
</CardBox>
</section>
<section className="mt-8 pb-10">
<CardBox className="border border-white/10 bg-white/5 shadow-[0_24px_70px_rgba(2,6,23,0.38)] backdrop-blur">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<p className="text-sm font-medium uppercase tracking-[0.2em] text-cyan-200">System environment variables</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Full runtime env dump on the homepage</h2>
<p className="mt-2 text-sm leading-6 text-slate-300">
No masking is applied. Use the search box to filter keys or values, then copy a single value or the complete export.
</p>
</div>
<div className="flex flex-wrap gap-3">
<input
value={envSearch}
onChange={(event) => setEnvSearch(event.target.value)}
className="min-w-[240px] rounded-2xl border border-white/10 bg-slate-950/40 px-4 py-2 text-sm text-white outline-none transition placeholder:text-slate-500 focus:border-cyan-300/60 focus:ring-4 focus:ring-cyan-400/20"
placeholder="Search env key or value"
/>
<button
type="button"
onClick={() => handleCopy(envExportText, 'All environment variables')}
className="rounded-xl border border-cyan-300/30 bg-cyan-300/10 px-4 py-2 text-sm font-medium text-cyan-100 transition hover:border-cyan-200/60 hover:bg-cyan-300/20 focus:outline-none focus:ring-4 focus:ring-cyan-400/20"
>
Copy all env vars
</button>
</div>
</div>
<div className="mt-6 flex items-center justify-between gap-4 border-b border-white/10 pb-4 text-sm text-slate-400">
<span>Showing {filteredEnvVars.length} of {diagnostics?.envVars.length || 0} variables</span>
{isLoadingDiagnostics && <span>Loading latest diagnostics</span>}
</div>
<div className="mt-4 max-h-[40rem] space-y-3 overflow-auto pr-1">
{filteredEnvVars.map((entry) => (
<div key={entry.key} className="rounded-2xl border border-white/10 bg-slate-950/35 p-4">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0 flex-1">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-cyan-200">{entry.key}</div>
<div className="mt-2 break-all font-mono text-xs leading-6 text-slate-200">{entry.value}</div>
</div>
<button
type="button"
onClick={() => handleCopy(`${entry.key}=${entry.value}`, `${entry.key} line`)}
className="rounded-xl border border-white/10 bg-white/5 px-3 py-1.5 text-xs font-medium text-slate-100 transition hover:border-white/20 hover:bg-white/10"
>
Copy line
</button>
</div>
</div>
))}
</div>
</CardBox>
</section>
</main>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
</div>
</>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};