Initial portal commit: landing + 9 AI-powered apps
Apps: - dwg-rooms: extract room numbers from DWG/DXF - dwg-counting: count symbols in PDF drawings (OpenCV template matching) - contract-check: review PDF contracts against a checklist (Claude vision + Tesseract OCR fallback) - email-drafter: bullet notes → polished Czech/English business emails - invoice-extractor: PDF/image invoice → structured data → Excel - translator: Czech-first translator across 19 languages with tone control - vv-check: find inconsistent unit prices across VV sheets in one workbook - vv-compare: diff original vs new VV files (changes / added / removed) - feature-request: portal users submit ideas + sample files Infrastructure: - LiteLLM gateway with per-app virtual keys + budgets - Langfuse observability - Geist font, shared theme, cross-subdomain back link + theme sync via cookie/URL - Caddy reverse proxy on *.klas.chat
This commit is contained in:
3
landing/src/app/api/auth/[...nextauth]/route.ts
Normal file
3
landing/src/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { handlers } from "@/auth";
|
||||
|
||||
export const { GET, POST } = handlers;
|
||||
BIN
landing/src/app/favicon.ico
Normal file
BIN
landing/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
155
landing/src/app/globals.css
Normal file
155
landing/src/app/globals.css
Normal file
@@ -0,0 +1,155 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
/* Dify / Untitled UI palette */
|
||||
--color-bg-primary: #ffffff;
|
||||
--color-bg-secondary: #f9fafb;
|
||||
--color-bg-tertiary: #f2f4f7;
|
||||
--color-bg-section: #f6f7f9;
|
||||
|
||||
--color-text-primary: #101828;
|
||||
--color-text-secondary: #354052;
|
||||
--color-text-tertiary: #676f83;
|
||||
--color-text-quaternary: #98a2b2;
|
||||
--color-text-disabled: #d0d5dc;
|
||||
|
||||
--color-border-subtle: rgb(16 24 40 / 0.04);
|
||||
--color-border-default: rgb(16 24 40 / 0.08);
|
||||
--color-border-hover: rgb(16 24 40 / 0.14);
|
||||
--color-border-strong: #d0d5dc;
|
||||
|
||||
--color-card: #ffffff;
|
||||
--color-card-hover: #f9fafb;
|
||||
|
||||
/* Dify primary blue */
|
||||
--color-primary-50: #eff4ff;
|
||||
--color-primary-100: #d1e0ff;
|
||||
--color-primary-200: #b2caff;
|
||||
--color-primary-500: #2970ff;
|
||||
--color-primary-600: #155aef;
|
||||
--color-primary-700: #004aeb;
|
||||
--color-primary-800: #00359e;
|
||||
|
||||
--color-primary: var(--color-primary-600);
|
||||
--color-primary-hover: var(--color-primary-700);
|
||||
--color-primary-foreground: #ffffff;
|
||||
|
||||
/* Accent palette for tile icons (also re-declared in :root below so
|
||||
Tailwind v4 doesn't tree-shake them — they're only used via inline
|
||||
style in tile.tsx, which Tailwind's purger can't see). */
|
||||
--color-accent-blue: #155aef;
|
||||
--color-accent-indigo: #444ce7;
|
||||
--color-accent-violet: #7a5af8;
|
||||
--color-accent-cyan: #0ba5ec;
|
||||
--color-accent-emerald: #17b26a;
|
||||
--color-accent-amber: #f79009;
|
||||
--color-accent-rose: #f04438;
|
||||
--color-accent-gray: #475467;
|
||||
|
||||
--color-destructive: #d92d20;
|
||||
--color-destructive-bg: #fef3f2;
|
||||
--color-destructive-border: #fda29b;
|
||||
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
|
||||
--shadow-card: 0 1px 2px rgb(16 24 40 / 0.05);
|
||||
--shadow-card-hover: 0 12px 24px -8px rgb(16 24 40 / 0.12), 0 4px 8px -4px rgb(16 24 40 / 0.06);
|
||||
|
||||
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: var(--font-geist-mono), ui-monospace, SFMono-Regular, monospace;
|
||||
}
|
||||
|
||||
/* Re-declare accents in :root so they survive Tailwind v4's purge of unused
|
||||
@theme variables. Used by inline styles in tile.tsx (color-mix). */
|
||||
:root {
|
||||
--color-accent-blue: #155aef;
|
||||
--color-accent-indigo: #444ce7;
|
||||
--color-accent-violet: #7a5af8;
|
||||
--color-accent-cyan: #0ba5ec;
|
||||
--color-accent-emerald: #17b26a;
|
||||
--color-accent-amber: #f79009;
|
||||
--color-accent-rose: #f04438;
|
||||
--color-accent-gray: #475467;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-bg-primary: #14181f;
|
||||
--color-bg-secondary: #1a1f29;
|
||||
--color-bg-tertiary: #232936;
|
||||
--color-bg-section: #181d26;
|
||||
|
||||
--color-text-primary: #f5f7fa;
|
||||
--color-text-secondary: #c8ccd5;
|
||||
--color-text-tertiary: #98a2b2;
|
||||
--color-text-quaternary: #676f83;
|
||||
--color-text-disabled: #4a525e;
|
||||
|
||||
--color-border-subtle: rgb(255 255 255 / 0.05);
|
||||
--color-border-default: rgb(255 255 255 / 0.08);
|
||||
--color-border-hover: rgb(255 255 255 / 0.14);
|
||||
--color-border-strong: #354052;
|
||||
|
||||
--color-card: #1a1f29;
|
||||
--color-card-hover: #232936;
|
||||
|
||||
--color-primary: var(--color-primary-500);
|
||||
--color-primary-hover: #4187ff;
|
||||
|
||||
--shadow-card: 0 1px 2px rgb(0 0 0 / 0.4);
|
||||
--shadow-card-hover: 0 12px 24px -8px rgb(0 0 0 / 0.5), 0 4px 8px -4px rgb(0 0 0 / 0.3);
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Dify-style typography utilities */
|
||||
.system-2xs-medium-uppercase {
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.system-xs-regular {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.system-xs-medium {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.system-sm-medium {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.system-sm-semibold {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Scrollbar polish */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
37
landing/src/app/layout.tsx
Normal file
37
landing/src/app/layout.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin", "latin-ext"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin", "latin-ext"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Colsys AI",
|
||||
description: "Interní AI portál společnosti Colsys.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="cs"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body className="min-h-full flex flex-col">
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
114
landing/src/app/login/page.tsx
Normal file
114
landing/src/app/login/page.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { auth, signIn } from "@/auth";
|
||||
import { Brand } from "@/components/brand";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
type SearchParams = Promise<{ callbackUrl?: string; error?: string }>;
|
||||
|
||||
export default async function LoginPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParams;
|
||||
}) {
|
||||
const { callbackUrl, error } = await searchParams;
|
||||
|
||||
const session = await auth();
|
||||
if (session?.user) {
|
||||
redirect(callbackUrl || "/");
|
||||
}
|
||||
|
||||
const hasEntra =
|
||||
!!process.env.AUTH_MICROSOFT_ENTRA_ID_ID &&
|
||||
!!process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET;
|
||||
const hasDevPassword = !!process.env.DEV_PORTAL_PASSWORD;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full flex-col bg-[var(--color-bg-secondary)]">
|
||||
<div className="flex flex-1 items-center justify-center px-4 py-12">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="mb-6 flex justify-center">
|
||||
<Brand size="lg" />
|
||||
</div>
|
||||
<div className="rounded-xl border-[0.5px] border-[var(--color-border-default)] bg-[var(--color-card)] p-7 shadow-[var(--shadow-card)]">
|
||||
<h1 className="text-lg font-semibold text-[var(--color-text-primary)]">
|
||||
Přihlášení
|
||||
</h1>
|
||||
<p className="system-xs-regular mt-1 text-[var(--color-text-tertiary)]">
|
||||
Pokračujte přihlášením přes firemní účet.
|
||||
</p>
|
||||
|
||||
{error ? (
|
||||
<div className="mt-5 rounded-lg border-[0.5px] border-[var(--color-destructive-border)] bg-[var(--color-destructive-bg)] px-3 py-2.5 text-xs text-[var(--color-destructive)]">
|
||||
Přihlášení se nezdařilo. Zkuste to prosím znovu.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{hasEntra ? (
|
||||
<form
|
||||
className="mt-6"
|
||||
action={async () => {
|
||||
"use server";
|
||||
await signIn("microsoft-entra-id", {
|
||||
redirectTo: callbackUrl || "/",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button type="submit" className="w-full" size="lg">
|
||||
Pokračovat přes Microsoft
|
||||
</Button>
|
||||
</form>
|
||||
) : null}
|
||||
|
||||
{hasEntra && hasDevPassword ? (
|
||||
<div className="my-6 flex items-center gap-3 text-xs text-[var(--color-text-quaternary)]">
|
||||
<span className="h-px flex-1 bg-[var(--color-border-default)]" />
|
||||
nebo
|
||||
<span className="h-px flex-1 bg-[var(--color-border-default)]" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{hasDevPassword ? (
|
||||
<form
|
||||
className="mt-6 flex flex-col gap-3"
|
||||
action={async (formData: FormData) => {
|
||||
"use server";
|
||||
await signIn("dev", {
|
||||
password: formData.get("password"),
|
||||
redirectTo: callbackUrl || "/",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<label className="system-xs-medium text-[var(--color-text-secondary)]">
|
||||
Vývojový přístup
|
||||
</label>
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Přístupové heslo"
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
<Button type="submit" variant="secondary" size="lg">
|
||||
Přihlásit se heslem
|
||||
</Button>
|
||||
</form>
|
||||
) : null}
|
||||
|
||||
{!hasEntra && !hasDevPassword ? (
|
||||
<p className="mt-6 rounded-lg bg-[var(--color-bg-tertiary)] p-4 text-sm text-[var(--color-text-tertiary)]">
|
||||
Není nakonfigurován žádný poskytovatel přihlášení. Nastavte{" "}
|
||||
<code className="font-mono text-xs">DEV_PORTAL_PASSWORD</code>{" "}
|
||||
nebo Microsoft Entra v prostředí.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<p className="mt-6 text-center text-xs text-[var(--color-text-quaternary)]">
|
||||
Interní AI portál společnosti Colsys
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
landing/src/app/page.tsx
Normal file
33
landing/src/app/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Header } from "@/components/header";
|
||||
import { TileGrid } from "@/components/tile-grid";
|
||||
import apps from "@/data/apps.json";
|
||||
import type { AppTile } from "@/components/tile";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex min-h-full flex-col bg-[var(--color-bg-secondary)]">
|
||||
<Header />
|
||||
<main className="mx-auto flex w-full max-w-7xl flex-1 flex-col gap-7 px-4 py-8 sm:px-8 sm:py-10">
|
||||
<section className="flex flex-col gap-2">
|
||||
<span className="system-2xs-medium-uppercase text-[var(--color-primary)]">
|
||||
Interní AI portál
|
||||
</span>
|
||||
<h1 className="text-2xl font-semibold tracking-tight text-[var(--color-text-primary)] sm:text-3xl">
|
||||
Vaše AI nástroje na jednom místě
|
||||
</h1>
|
||||
<p className="max-w-2xl text-sm text-[var(--color-text-tertiary)] sm:text-base">
|
||||
Vyberte si průvodce úlohou, nebo zahajte volný chat. Vše běží na
|
||||
firemních klíčích — s rozpočty, sledováním nákladů a bez exportu
|
||||
vašich dat ven.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<TileGrid apps={apps as AppTile[]} />
|
||||
</main>
|
||||
<footer className="mx-auto w-full max-w-7xl px-4 pb-10 pt-2 text-xs text-[var(--color-text-quaternary)] sm:px-8">
|
||||
Nevkládejte do AI nástrojů hesla ani neredaktované osobní či citlivé
|
||||
údaje.
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user