Directory structure:
└── frontend/
    ├── README.md
    ├── components.json
    ├── Dockerfile
    ├── env.ts
    ├── next.config.mjs
    ├── package.json
    ├── postcss.config.mjs
    ├── tailwind.config.ts
    ├── tsconfig.json
    ├── .eslintrc.json
    ├── app/
    │   ├── globals.css
    │   ├── layout.tsx
    │   └── (analysis)/
    │       ├── audio-timeline.tsx
    │       ├── captions.tsx
    │       ├── controls-bar.tsx
    │       ├── file-picker.tsx
    │       ├── page.tsx
    │       ├── store.ts
    │       ├── time-slider.tsx
    │       ├── video-player.tsx
    │       └── panels/
    │           ├── errors.tsx
    │           ├── readibility-score.tsx
    │           └── summary.tsx
    ├── components/
    │   ├── dropzone.tsx
    │   ├── video-thumbnail.tsx
    │   └── ui/
    │       ├── border-beam.tsx
    │       ├── button.tsx
    │       ├── label.tsx
    │       ├── popover.tsx
    │       ├── select.tsx
    │       ├── switch.tsx
    │       └── tooltip.tsx
    └── lib/
        ├── config.ts
        └── utils.ts

================================================
FILE: frontend/README.md
================================================
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.



================================================
FILE: frontend/components.json
================================================
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  }
}


================================================
FILE: frontend/Dockerfile
================================================
FROM node:18-alpine

WORKDIR /usr/src/app

COPY . .
RUN npm install
ENV NEXT_PUBLIC_API_URL="http://159.69.15.185:8000"
RUN npm run build

COPY . /usr/src/app
EXPOSE 3000
CMD [ "npm", "run", "start" ]


================================================
FILE: frontend/env.ts
================================================
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  server: {},
  client: {
    NEXT_PUBLIC_API_URL: z.string().url(),
  },
  experimental__runtimeEnv: {
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  },
});



================================================
FILE: frontend/next.config.mjs
================================================
import { fileURLToPath } from "node:url";
import { createJiti } from "jiti";
const jiti = createJiti(fileURLToPath(import.meta.url));

jiti("./env");

/** @type {import('next').NextConfig} */
const nextConfig = {};

export default nextConfig;



================================================
FILE: frontend/package.json
================================================
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@phosphor-icons/react": "^2.1.7",
    "@radix-ui/react-icons": "^1.3.0",
    "@radix-ui/react-label": "^2.1.0",
    "@radix-ui/react-popover": "^1.1.1",
    "@radix-ui/react-select": "^2.1.1",
    "@radix-ui/react-slider": "^1.2.0",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-switch": "^1.1.0",
    "@radix-ui/react-tooltip": "^1.1.2",
    "@t3-oss/env-nextjs": "^0.11.1",
    "@vidstack/react": "^1.12.11",
    "axios": "^1.7.7",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "framer-motion": "^11.9.0",
    "lucide-react": "^0.446.0",
    "motion-number": "^0.1.7",
    "next": "14.2.13",
    "react": "^18",
    "react-audio-visualize": "^1.2.0",
    "react-dom": "^18",
    "react-dropzone": "^14.2.3",
    "tailwind-merge": "^2.5.2",
    "tailwindcss-animate": "^1.0.7",
    "zod": "^3.23.8",
    "zustand": "^5.0.0-rc.2"
  },
  "devDependencies": {
    "@types/axios": "^0.14.0",
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.13",
    "jiti": "^2.0.0",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "tailwindcss-bg-patterns": "^0.3.0",
    "typescript": "^5"
  }
}



================================================
FILE: frontend/postcss.config.mjs
================================================
/** @type {import('postcss-load-config').Config} */
const config = {
  plugins: {
    tailwindcss: {},
  },
};

export default config;



================================================
FILE: frontend/tailwind.config.ts
================================================
import type { Config } from "tailwindcss";

const config: Config = {
  darkMode: ["class"],
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      fontFamily: {
        sans: ["var(--font-inter)"],
      },
      colors: {
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        card: {
          DEFAULT: "hsl(var(--card))",
          foreground: "hsl(var(--card-foreground))",
        },
        popover: {
          DEFAULT: "hsl(var(--popover))",
          foreground: "hsl(var(--popover-foreground))",
        },
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary))",
          foreground: "hsl(var(--secondary-foreground))",
        },
        muted: {
          DEFAULT: "hsl(var(--muted))",
          foreground: "hsl(var(--muted-foreground))",
        },
        accent: {
          DEFAULT: "hsl(var(--accent))",
          foreground: "hsl(var(--accent-foreground))",
        },
        destructive: {
          DEFAULT: "hsl(var(--destructive))",
          foreground: "hsl(var(--destructive-foreground))",
        },
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        chart: {
          "1": "hsl(var(--chart-1))",
          "2": "hsl(var(--chart-2))",
          "3": "hsl(var(--chart-3))",
          "4": "hsl(var(--chart-4))",
          "5": "hsl(var(--chart-5))",
        },
      },
      borderRadius: {
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
      animation: {
        "border-beam": "border-beam calc(var(--duration)*1s) infinite linear",
        shine: "shine var(--duration) infinite linear",
        "fade-in": "fade-in 0.6s ease-out",
      },
      keyframes: {
        "border-beam": {
          "100%": {
            "offset-distance": "100%",
          },
        },
        shine: {
          "0%": {
            "background-position": "0% 0%",
          },
          "50%": {
            "background-position": "100% 100%",
          },
          to: {
            "background-position": "0% 0%",
          },
        },
        "fade-in": {
          "0%": { opacity: "0" },
          "100%": { opacity: "1" },
        },
      },
      backgroundImage: {
        "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
      },
    },
  },
  plugins: [
    require("@vidstack/react/tailwind.cjs"),
    require("tailwindcss-animate"),
    require("tailwindcss-bg-patterns"),
  ],
};
export default config;



================================================
FILE: frontend/tsconfig.json
================================================
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}



================================================
FILE: frontend/.eslintrc.json
================================================
{
  "extends": ["next/core-web-vitals", "next/typescript"]
}



================================================
FILE: frontend/app/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;

/* @media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
} */

body {
  font-family: Arial, Helvetica, sans-serif;
}

@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
}

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 0 0% 3.9%;
    --card: 0 0% 100%;
    --card-foreground: 0 0% 3.9%;
    --popover: 0 0% 100%;
    --popover-foreground: 0 0% 3.9%;
    --primary: 0 0% 9%;
    --primary-foreground: 0 0% 98%;
    --secondary: 0 0% 96.1%;
    --secondary-foreground: 0 0% 9%;
    --muted: 0 0% 96.1%;
    --muted-foreground: 0 0% 45.1%;
    --accent: 0 0% 96.1%;
    --accent-foreground: 0 0% 9%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 0 0% 98%;
    --border: 0 0% 89.8%;
    --input: 0 0% 89.8%;
    --ring: 0 0% 3.9%;
    --chart-1: 12 76% 61%;
    --chart-2: 173 58% 39%;
    --chart-3: 197 37% 24%;
    --chart-4: 43 74% 66%;
    --chart-5: 27 87% 67%;
    --radius: 0.5rem;
  }
  .dark {
    --background: 0 0% 3.9%;
    --foreground: 0 0% 98%;
    --card: 0 0% 3.9%;
    --card-foreground: 0 0% 98%;
    --popover: 0 0% 3.9%;
    --popover-foreground: 0 0% 98%;
    --primary: 0 0% 98%;
    --primary-foreground: 0 0% 9%;
    --secondary: 0 0% 14.9%;
    --secondary-foreground: 0 0% 98%;
    --muted: 0 0% 14.9%;
    --muted-foreground: 0 0% 63.9%;
    --accent: 0 0% 14.9%;
    --accent-foreground: 0 0% 98%;
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 0 0% 98%;
    --border: 0 0% 14.9%;
    --input: 0 0% 14.9%;
    --ring: 0 0% 83.1%;
    --chart-1: 220 70% 50%;
    --chart-2: 160 60% 45%;
    --chart-3: 30 80% 55%;
    --chart-4: 280 65% 60%;
    --chart-5: 340 75% 55%;
  }
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}



================================================
FILE: frontend/app/layout.tsx
================================================
import type { Metadata } from "next";
import { Inter } from "next/font/google";

import "@vidstack/react/player/styles/base.css";
import "./globals.css";

const inter = Inter({
  subsets: ["latin-ext"],
  variable: "--font-inter",
});

export const metadata: Metadata = {
  title: "Analiza wideo",
  description: "Analiza wideo",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="pl">
      <body className={`${inter.variable} antialiased font-sans`}>
        {children}
      </body>
    </html>
  );
}



================================================
FILE: frontend/app/(analysis)/audio-timeline.tsx
================================================
"use client";

import { useAnalysisStore } from "./store";
import { AudioVisualizer } from "react-audio-visualize";
import { useMediaState } from "@vidstack/react";
import { motion } from "framer-motion";
import { SECOND_WIDTH } from "@/lib/config";

export function AudioTimeline() {
  const file = useAnalysisStore((state) => state.file);
  const player = useAnalysisStore((state) => state.player);
  const results = useAnalysisStore((state) => state.results);
  const currentTime = useMediaState("currentTime", player);
  const duration = useMediaState("duration", player);

  if (!file) return null;

  return (
    <div className="relative overflow-hidden w-full h-full animate-fade-in">
      <motion.div
        className="h-[64px] relative left-1/2 transition-transform will-change-transform"
        style={{
          transform: `translateX(-${Math.round(currentTime * SECOND_WIDTH)}px)`,
        }}
      >
        <AudioVisualizer
          key={duration}
          blob={file}
          width={duration * SECOND_WIDTH}
          height={64}
          barWidth={2}
          gap={0}
          barColor={"#00000022"}
          style={{ position: "absolute", left: 0, top: 0 }}
        />
        {results?.long_pauses?.map((pause) => (
          <div
            key={pause.start_time}
            className="absolute top-[50%] -translate-y-[50%] h-2/3 bg-rose-400/20 border border-rose-300 rounded-md px-0.5 hover:bg-rose-200 transition-colors cursor-pointer before:absolute before:inset-0 before:pattern-diagonal-lines before:pattern-rose-400 before:pattern-bg-rose-100 before:pattern-size-8 before:pattern-opacity-10"
            style={{
              left: `${pause.start_time * SECOND_WIDTH}px`,
              width: `${pause.duration * SECOND_WIDTH}px`,
            }}
          >
            <div className="text-xs text-rose-600 font-medium z-20">
              Długa pauza
            </div>
          </div>
        ))}
        {results?.quiet_segments?.map((segment) => (
          <div
            key={segment.start_time}
            className="absolute top-[50%] -translate-y-[50%] h-2/3 bg-rose-400/20 border border-rose-300 rounded-md px-0.5 hover:bg-rose-200 transition-colors cursor-pointer before:absolute before:inset-0 before:pattern-diagonal-lines before:pattern-rose-400 before:pattern-bg-rose-100 before:pattern-size-8 before:pattern-opacity-10"
            style={{
              left: `${segment.start_time * SECOND_WIDTH}px`,
              width: `${segment.duration * SECOND_WIDTH}px`,
            }}
          >
            <div className="text-xs text-rose-600 font-medium z-20">
              Mówienie zbyt cicho
            </div>
          </div>
        ))}
        {results?.loud_segments?.map((segment) => (
          <div
            key={segment.start_time}
            className="absolute top-[50%] -translate-y-[50%] h-2/3 bg-rose-400/20 border border-rose-300 rounded-md px-0.5 hover:bg-rose-200 transition-colors cursor-pointer before:absolute before:inset-0 before:pattern-diagonal-lines before:pattern-rose-400 before:pattern-bg-rose-100 before:pattern-size-8 before:pattern-opacity-10"
            style={{
              left: `${segment.start_time * SECOND_WIDTH}px`,
              width: `${segment.duration * SECOND_WIDTH}px`,
            }}
          >
            <div className="text-xs text-rose-600 font-medium z-20">
              Mówienie zbyt głośno
            </div>
          </div>
        ))}
      </motion.div>
      {/* <div className="absolute bottom-0 left-0 h-full flex items-center space-x-2 bg-white/90 backdrop-blur-sm pr-4">
        <Select value="volume">
          <SelectTrigger className="w-[180px]">
            <SelectValue placeholder="Wybierz cechę" />
          </SelectTrigger>
          <SelectContent>
            <SelectGroup>
              <SelectItem value="pauses">Pauzy</SelectItem>
              <SelectItem value="volume">Głośność</SelectItem>
              <SelectItem value="pitch">Ton</SelectItem>
              <SelectItem value="speed">Szybkość</SelectItem>
            </SelectGroup>
          </SelectContent>
        </Select>
      </div> */}
    </div>
  );
}



================================================
FILE: frontend/app/(analysis)/captions.tsx
================================================
import { formatTime, useMediaState } from "@vidstack/react";
import { useAnalysisStore } from "./store";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import { Fragment, useState } from "react";
import { Swap } from "@phosphor-icons/react/dist/ssr";
import { SECOND_WIDTH } from "@/lib/config";

export function Captions() {
  const player = useAnalysisStore((state) => state.player);
  const results = useAnalysisStore((state) => state.results);
  const currentTime = useMediaState("currentTime", player);

  return (
    <div className="bg-neutral-100 animate-fade-in rounded-xl h-[64px] gap-1 flex flex-wrap items-center justify-center w-full relative shadow border border-neutral-200 text-center overflow-hidden">
      <motion.div
        className="relative w-full h-full left-1/2 transition-transform py-2 px-3 will-change-transform"
        style={{
          transform: `translateX(-${Math.round(currentTime * SECOND_WIDTH)}px)`,
        }}
      >
        {results?.words.map((word, index) => {
          const isJargon = results?.jargon?.includes(word.word.toLowerCase());
          const isNonPolishLanguage = results?.non_polish_language?.includes(
            word.word.toLowerCase()
          );
          const isPassiveVoice = results?.passive_voice?.includes(
            word.word.toLowerCase()
          );
          const isNonexistentWord = results?.nonexistent_words?.includes(
            word.word.toLowerCase()
          );
          const isRepetition = results?.repetitions?.includes(index);
          const isError =
            isJargon ||
            isNonPolishLanguage ||
            isPassiveVoice ||
            isNonexistentWord ||
            isRepetition;

          const component = (
            <div
              className={cn(
                "absolute border rounded-md px-0.5 top-1/2 overflow-hidden transition-opacity",
                isError
                  ? "bg-rose-100 border-rose-300 hover:bg-rose-200 text-rose-700 before:absolute before:inset-0 before:pattern-diagonal-lines before:pattern-rose-400 before:pattern-bg-rose-100 before:pattern-size-8 before:pattern-opacity-10"
                  : "bg-neutral-900/10 border-neutral-150"
              )}
              style={{
                transform: `translateX(${
                  word.start_time * SECOND_WIDTH
                }px) translateY(-50%)`,
                width: `${(word.end_time - word.start_time) * SECOND_WIDTH}px`,
                opacity: currentTime > word.start_time ? 1 : 0.5,
              }}
            >
              {word.word}
            </div>
          );

          if (isError) {
            return (
              <ErrorPopover
                key={index}
                time={word.start_time}
                currentTime={currentTime}
                description={[
                  isJargon && "Żargon",
                  isNonPolishLanguage && "Obcy język",
                  isPassiveVoice && "Strona bierna",
                  isNonexistentWord && "Nieistniejące słowo",
                  isRepetition && "Powtórzenie",
                ]
                  .filter(Boolean)
                  .join(", ")}
              >
                {component}
              </ErrorPopover>
            );
          }

          return <Fragment key={index}>{component}</Fragment>;
        })}
        {results?.topic_changes?.map((topicChange, index) => (
          <ErrorPopover
            key={index}
            time={results.words[topicChange]?.start_time}
            currentTime={currentTime}
            description={"Zmiana tematu"}
          >
            <div
              key={index}
              className="top-[1px] bg-rose-600 w-4 h-4 flex items-center transition-opacity justify-center rounded-full absolute"
              style={{
                transform: `translateX(${
                  results.words[topicChange]?.start_time * SECOND_WIDTH
                }px)`,
                opacity:
                  currentTime > results.words[topicChange]?.start_time
                    ? 1
                    : 0.5,
              }}
            >
              <Swap className="w-3 h-3 text-rose-50" weight="bold" />
            </div>
          </ErrorPopover>
        ))}
      </motion.div>
    </div>
  );
}

function ErrorPopover({
  time,
  currentTime,
  description,
  children,
}: {
  time: number;
  currentTime: number;
  description: string;
  children: React.ReactNode;
}) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <Popover
      open={isOpen || (currentTime > time && currentTime < time + 1)}
      onOpenChange={setIsOpen}
    >
      <PopoverTrigger asChild>{children}</PopoverTrigger>
      <PopoverContent side="top" className="w-[200px] flex flex-col gap-1">
        <p className="text-xs font-medium text-rose-600">Błąd</p>
        <p className="text-sm">{description}</p>
        <p className="text-xs text-neutral-500 tabular-nums">
          {formatTime(time)}
        </p>
      </PopoverContent>
    </Popover>
  );
}



================================================
FILE: frontend/app/(analysis)/controls-bar.tsx
================================================
"use client";

import { Button } from "@/components/ui/button";
import {
  ArrowLineLeft,
  Pause,
  Play,
  SpeakerSimpleHigh,
  SpeakerSimpleSlash,
} from "@phosphor-icons/react/dist/ssr";

import { TimeSlider } from "./time-slider";
import { formatTime, useMediaRemote, useMediaState } from "@vidstack/react";
import { useAnalysisStore } from "./store";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";

export function ControlsBar() {
  const player = useAnalysisStore((state) => state.player);
  const time = useMediaState("currentTime", player);
  const duration = useMediaState("duration", player);
  const isPaused = useMediaState("paused", player);
  const isMuted = useMediaState("muted", player);
  const remote = useMediaRemote(player);

  return (
    <div className="grid grid-cols-3 gap-4 animate-fade-in h-[64px] bg-neutral-900/80 items-center justify-between rounded-full shadow border border-neutral-200 px-4 py-2 pb-3 relative overflow-hidden">
      <div className="text-accent/80 text-xs font-medium tabular-nums">
        {formatTime(time)} / {formatTime(duration)}
      </div>
      <div className="flex items-center justify-center gap-2">
        <TooltipProvider>
          <Tooltip>
            <TooltipTrigger asChild>
              <Button
                size="icon"
                variant="ghost"
                className="rounded-full hover:bg-accent/10 hover:text-accent text-accent/70"
                onClick={() => remote.seek(0)}
                disabled={time === 0}
              >
                <ArrowLineLeft weight="bold" className="w-5 h-5" />
              </Button>
            </TooltipTrigger>
            <TooltipContent>
              <p>Przewiń do początku</p>
            </TooltipContent>
          </Tooltip>
          <Tooltip>
            <TooltipTrigger asChild>
              <Button
                size="icon"
                variant="secondary"
                className="rounded-full"
                onClick={() => (isPaused ? remote.play() : remote.pause())}
              >
                {isPaused ? (
                  <Play weight="bold" className="w-5 h-5" />
                ) : (
                  <Pause weight="bold" className="w-5 h-5" />
                )}
              </Button>
            </TooltipTrigger>{" "}
            <TooltipContent>
              <p>{isPaused ? "Wznów" : "Wstrzymaj"}</p>
            </TooltipContent>
          </Tooltip>

          <Tooltip>
            <TooltipTrigger asChild>
              <Button
                size="icon"
                variant="ghost"
                className="rounded-full hover:bg-accent/10 hover:text-accent text-accent/70"
                onClick={() => (isMuted ? remote.unmute() : remote.mute())}
              >
                {isMuted ? (
                  <SpeakerSimpleSlash weight="bold" className="w-5 h-5" />
                ) : (
                  <SpeakerSimpleHigh weight="bold" className="w-5 h-5" />
                )}
              </Button>
            </TooltipTrigger>
            <TooltipContent>
              <p>{isMuted ? "Odcisz" : "Wycisz"}</p>
            </TooltipContent>
          </Tooltip>
        </TooltipProvider>
      </div>
      <div className=""></div>
      <TimeSlider />
    </div>
  );
}



================================================
FILE: frontend/app/(analysis)/file-picker.tsx
================================================
"use client";

import { Dropzone } from "@/components/dropzone";
import { useAnalysisStore } from "./store";

export function FilePicker() {
  const setFile = useAnalysisStore((state) => state.setFile);

  return (
    <div className="container mx-auto min-h-screen flex flex-col items-center justify-center gap-10 pt-16">
      <div className="flex flex-col items-center justify-center gap-3">
        <h1 className="text-4xl font-medium">Analizuj wideo</h1>
        <h2 className="text-lg text-neutral-600">
          Wybierz plik wideo do oceny jakości wypowiedzi
        </h2>
      </div>

      <Dropzone onFileSelect={(file) => setFile(file)} />
    </div>
  );
}



================================================
FILE: frontend/app/(analysis)/page.tsx
================================================
"use client";

import { useRef, useState } from "react";
import axios, { AxiosProgressEvent } from "axios";
import { env } from "@/env";
import MotionNumber from "motion-number";
import { SpinnerGap } from "@phosphor-icons/react/dist/ssr";

import { Button } from "@/components/ui/button";
import { AudioTimeline } from "./audio-timeline";
import { ControlsBar } from "./controls-bar";
import { useAnalysisStore } from "./store";
import { VideoPlayer } from "./video-player";
import { Captions } from "./captions";
import { ReadibilityScore } from "./panels/readibility-score";
import { Summary } from "./panels/summary";
import { FilePicker } from "./file-picker";
import { Errors } from "./panels/errors";

export default function Page() {
  const status = useAnalysisStore((state) => state.status);
  const setStatus = useAnalysisStore((state) => state.setStatus);
  const setResults = useAnalysisStore((state) => state.setResults);
  const abortController = useRef<AbortController>();
  const [fileProgress, setFileProgress] = useState(0);

  useAnalysisStore.subscribe(async (state) => {
    if (state.status === "before-upload") {
      abortController.current = new AbortController();

      setStatus("uploading");

      const formData = new FormData();
      formData.append("file", state.file as Blob);

      const request = await axios.post(
        `${env.NEXT_PUBLIC_API_URL}/upload`,
        formData,
        {
          headers: {
            "Content-Type": "multipart/form-data",
          },
          onUploadProgress: (progressEvent: AxiosProgressEvent) => {
            const newProgress =
              progressEvent.loaded / (progressEvent.total || 1);
            setFileProgress(newProgress);
            if (newProgress === 1) {
              setStatus("processing");
            }
          },
          signal: abortController.current.signal,
        }
      );

      const json = request.data;

      setResults(json);
      setStatus("ready");
    }
  });

  if (status === "empty") {
    return <FilePicker />;
  }

  return (
    <div className="w-screen grid grid-cols-1 h-screen gap-4 p-6 grid-rows-[1fr_auto_auto_64px] relative">
      <div className="flex items-center justify-center relative">
        <div className="absolute inset-0 bg-gradient-radial animate-pulse from-blue-500/30 to-transparent blur-2xl"></div>
        <VideoPlayer />
      </div>
      {status === "ready" ? (
        <>
          <div className="absolute animate-fade-in flex flex-col gap-4 left-6 top-6 z-20 w-[340px]">
            <Errors />
            <Summary />
          </div>
          <div className="absolute animate-fade-in flex flex-col gap-4 right-6 top-6 z-20 w-[340px]">
            <ReadibilityScore />
          </div>
          <Captions />
          <AudioTimeline />
          <ControlsBar />
        </>
      ) : null}
      {(status === "uploading" || status === "processing") && (
        <div className="flex flex-col gap-4 items-center justify-center">
          <div className="text-sm text-neutral-600 font-medium flex items-center gap-2">
            <SpinnerGap className="animate-spin w-4 h-4" weight="bold" />{" "}
            {status === "uploading" ? (
              <>
                Wgrywanie pliku...{" "}
                <MotionNumber
                  value={fileProgress}
                  format={{
                    notation: "compact",
                    style: "percent",
                  }}
                />
              </>
            ) : (
              "Przetwarzanie wideo..."
            )}
          </div>
          <Button
            variant="outline"
            onClick={() => {
              abortController.current?.abort();
              setStatus("empty");
            }}
          >
            Anuluj
          </Button>
        </div>
      )}
    </div>
  );
}



================================================
FILE: frontend/app/(analysis)/store.ts
================================================
import { MediaPlayerInstance } from "@vidstack/react";
import { createRef, RefObject } from "react";
import { create } from "zustand";

type Status = "empty" | "before-upload" | "uploading" | "processing" | "ready";

interface AnalysisState {
  status: Status;
  setStatus: (status: Status) => void;
  results?: {
    long_pauses: {
      start_time: number;
      end_time: number;
      duration: number;
    }[];
    quiet_segments: {
      start_time: number;
      end_time: number;
      duration: number;
    }[];
    loud_segments: {
      start_time: number;
      end_time: number;
      duration: number;
    }[];
    readability_score: number;
    words: {
      word: string;
      start_time: number;
      end_time: number;
      syllable_count: number;
    }[];
    repetitions: number[];
    topic_changes: number[];
    jargon: string[];
    nonexistent_words: string[];
    non_polish_language: string[];
    passive_voice: string[];
  };
  setResults: (results: AnalysisState["results"]) => void;
  file?: File;
  setFile: (file: File) => void;
  player: RefObject<MediaPlayerInstance>;
}

export const useAnalysisStore = create<AnalysisState>()((set) => ({
  status: "empty",
  player: createRef<MediaPlayerInstance>(),
  setFile: (file: File) => set({ file, status: "before-upload" }),
  setResults: (results: AnalysisState["results"]) => set({ results }),
  setStatus: (status: Status) => set({ status }),
}));



================================================
FILE: frontend/app/(analysis)/time-slider.tsx
================================================
"use client";

import { useEffect, useState } from "react";
import * as Slider from "@radix-ui/react-slider";
import {
  formatTime,
  useMediaRemote,
  useMediaState,
  useSliderPreview,
} from "@vidstack/react";
import { useAnalysisStore } from "./store";

export function TimeSlider() {
  const player = useAnalysisStore((state) => state.player);
  const time = useMediaState("currentTime", player);
  const canSeek = useMediaState("canSeek", player);
  const duration = useMediaState("duration", player);
  const seeking = useMediaState("seeking", player);
  const remote = useMediaRemote(player);
  const step = (1 / duration) * 100;
  const [value, setValue] = useState(0);
  const { previewRootRef, previewRef, previewValue } = useSliderPreview({
    clamp: true,
    offset: 6,
    orientation: "horizontal",
  });
  const previewTime = (previewValue / 100) * duration;

  // Keep slider value in-sync with playback.
  useEffect(() => {
    if (seeking) return;
    setValue((time / duration) * 100);
  }, [time, duration, seeking]);

  return (
    <Slider.Root
      className="group absolute bottom-0 px-3 inline-flex w-full cursor-pointer touch-none select-none items-center outline-none"
      value={[value]}
      disabled={!canSeek}
      step={Number.isFinite(step) ? step : 1}
      ref={previewRootRef}
      onValueChange={([value]) => {
        setValue(value);
        remote.seeking((value / 100) * duration);
      }}
      onValueCommit={([value]) => {
        remote.seek((value / 100) * duration);
      }}
    >
      <Slider.Track className="h-[5px] w-full rounded-sm bg-white/30 relative">
        <Slider.Range className="bg-blue-400 absolute h-full rounded-sm will-change-[width]" />
      </Slider.Track>

      {/* <Slider.Thumb
        aria-label="Current Time"
        className="block h-[15px] w-[15px] bottom-0 rounded-full border border-blue-200 bg-blue-400 outline-none opacity-0 ring-blue-400/40 transition-opacity group-hocus:opacity-100 focus:opacity-100 focus:ring-4 will-change-[left]"
      /> */}

      <div
        className="flex flex-col items-center justify-center absolute opacity-0 data-[visible]:opacity-100 transition-opacity duration-200 will-change-[left] pointer-events-none bg-neutral-900/50 h-6 tabular-nums rounded-full px-2 text-xs backdrop-blur-xl text-white/90 font-medium"
        ref={previewRef}
      >
        {formatTime(previewTime)}
      </div>
    </Slider.Root>
  );
}



================================================
FILE: frontend/app/(analysis)/video-player.tsx
================================================
"use client";

import { MediaPlayer, MediaProvider } from "@vidstack/react";
import { useAnalysisStore } from "./store";
import { memo } from "react";

export function VideoPlayerComponent() {
  const file = useAnalysisStore((state) => state.file);
  const player = useAnalysisStore((state) => state.player);

  if (!file) return null;

  return (
    <MediaPlayer
      className="w-[48%] aspect-video relative bg-slate-900 text-white shadow overflow-hidden rounded-lg ring-media-focus data-[focus]:ring-4"
      src={{
        src: URL.createObjectURL(file),
        type: "video/mp4",
      }}
      playsInline
      ref={player}
    >
      <MediaProvider></MediaProvider>
    </MediaPlayer>
  );
}

export const VideoPlayer = memo(VideoPlayerComponent);



================================================
FILE: frontend/app/(analysis)/panels/errors.tsx
================================================
import { Button } from "@/components/ui/button";
import { useAnalysisStore } from "../store";
import { formatTime, useMediaRemote } from "@vidstack/react";

function TimeMarker({ time }: { time: number }) {
  const player = useAnalysisStore((state) => state.player);
  const remote = useMediaRemote(player);

  return (
    <Button
      variant="ghost"
      className="px-1 h-auto py-0.5 text-blue-600 hover:bg-blue-600/10 hover:text-blue-600 rounded-full"
      size="sm"
      onClick={() => remote.seek(time)}
    >
      {formatTime(time)}
    </Button>
  );
}

export function Errors() {
  return (
    <div className="bg-white/90 backdrop-blur-lg shadow-sm border border-neutral-200 rounded-lg py-2 px-3 flex flex-col gap-1">
      <div className="text-accent-foreground/80 text-xs font-medium">
        Znalezione modyfikacje
      </div>
      <div className="flex">
        <div className="bg-neutral-200 px-2 py-1 shadow-sm rounded-full border border-neutral-300 text-sm">
          Mówienie zbyt głośno <TimeMarker time={28} />
        </div>
      </div>
    </div>
  );
}



================================================
FILE: frontend/app/(analysis)/panels/readibility-score.tsx
================================================
import { useMediaState } from "@vidstack/react";
import { useAnalysisStore } from "../store";
import MotionNumber from "motion-number";

export function ReadibilityScore() {
  const results = useAnalysisStore((state) => state.results);
  const player = useAnalysisStore((state) => state.player);
  const currentTime = useMediaState("currentTime", player);

  return (
    <div className="bg-white/90 backdrop-blur-lg shadow-sm border border-neutral-200 rounded-lg p-2 flex flex-col gap-1">
      <div className="text-accent-foreground/80 text-xs font-medium">
        Ocena prostości języka
      </div>
      <div className="text-5xl text-center text-yellow-500 my-2">
        {results?.readability_score}
      </div>
      <div className="text-xs text-center text-neutral-600">
        Współczynnik mglistości Gunninga
      </div>
      <div className="mt-2 pt-2 border-t grid grid-cols-2 border-neutral-200">
        <div className="flex flex-col items-center gap-1">
          <div className="text-2xl text-center text-blue-500 my-1">
            <MotionNumber
              value={
                results?.words.reduce(
                  (acc, word) => acc + (currentTime > word.start_time ? 1 : 0),
                  0
                ) ?? 0
              }
            />
          </div>
          <div className="text-neutral-600 text-xs">Liczba słów</div>
        </div>
        <div className="flex flex-col items-center gap-1">
          <div className="text-2xl text-center text-blue-500 my-1">2</div>
          <div className="text-neutral-600 text-xs">Liczba zdań</div>
        </div>
      </div>
    </div>
  );
}



================================================
FILE: frontend/app/(analysis)/panels/summary.tsx
================================================
import { useAnalysisStore } from "../store";

export function Summary() {
  const results = useAnalysisStore((state) => state.results);

  return (
    <div className="bg-white/90 backdrop-blur-lg shadow-sm border border-neutral-200 rounded-lg py-2 px-3 flex flex-col gap-1">
      <div className="text-accent-foreground/80 text-xs font-medium">
        Podsumowanie
      </div>
      <div className="text-sm">
        {results?.words.map((word) => word.word).join(" ")}
      </div>
    </div>
  );
}



================================================
FILE: frontend/components/dropzone.tsx
================================================
"use client";

import { useDropzone } from "react-dropzone";
import { BorderBeam } from "./ui/border-beam";
import { VideoThumbnail } from "./video-thumbnail";
import { useEffect } from "react";

interface DropzoneProps {
  onFileSelect: (file: File) => void;
}

export function Dropzone({ onFileSelect }: DropzoneProps) {
  const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
    multiple: false,
    accept: {
      "video/*": [".mp4", ".mov", ".avi", ".mkv"],
    },
  });

  useEffect(() => {
    if (acceptedFiles.length > 0) {
      onFileSelect(acceptedFiles[0]);
    }
  }, [acceptedFiles, onFileSelect]);

  return (
    <div
      {...getRootProps({
        className:
          "bg-neutral-100 rounded-xl min-h-[200px] flex p-2 flex-col items-center justify-center w-2/3 relative shadow border border-neutral-200",
      })}
    >
      <BorderBeam />
      <input {...getInputProps()} />

      {acceptedFiles.length > 0 && (
        <div className="w-full flex items-center justify-center gap-2">
          {acceptedFiles.map((file) => (
            <div
              key={file.name}
              className="bg-neutral-200 rounded-2xl w-1/2 flex items-center shadow-sm relative"
            >
              <VideoThumbnail file={file} />
              <div className="absolute top-2 max-w-[80%] left-1/2 -translate-x-1/2 bg-neutral-900/50 rounded-full px-3 text-xs backdrop-blur-xl text-white/90 font-medium h-5 flex items-center line-clamp-1 justify-center text-ellipsis overflow-hidden">
                {file.name}
              </div>
            </div>
          ))}
        </div>
      )}

      {acceptedFiles.length === 0 && (
        <p className="text-neutral-500 text-sm font-medium">
          Przeciągnij i upuść plik wideo, lub kliknij, aby wybrać plik
        </p>
      )}
    </div>
  );
}



================================================
FILE: frontend/components/video-thumbnail.tsx
================================================
"use client";

import { useCallback } from "react";

export function VideoThumbnail({ file }: { file: File }) {
  const setTime = useCallback((node: HTMLVideoElement) => {
    if (node) {
      try {
        // Set time for better thumbnail
        node.currentTime = 10;
      } catch (e) {
        console.error(e);
      }
    }
  }, []);

  return (
    <video
      className="w-full aspect-square object-cover rounded-xl border border-neutral-200"
      src={URL.createObjectURL(file)}
      ref={setTime}
      controls={false}
    />
  );
}



================================================
FILE: frontend/components/ui/border-beam.tsx
================================================
import { cn } from "@/lib/utils";

interface BorderBeamProps {
  className?: string;
  size?: number;
  duration?: number;
  borderWidth?: number;
  anchor?: number;
  colorFrom?: string;
  colorTo?: string;
  delay?: number;
}

export const BorderBeam = ({
  className,
  size = 200,
  duration = 15,
  anchor = 90,
  borderWidth = 2,
  colorFrom = "#389DEF",
  colorTo = "#B838EF",
  delay = 0,
}: BorderBeamProps) => {
  return (
    <div
      style={
        {
          "--size": size,
          "--duration": duration,
          "--anchor": anchor,
          "--border-width": borderWidth,
          "--color-from": colorFrom,
          "--color-to": colorTo,
          "--delay": `-${delay}s`,
        } as React.CSSProperties
      }
      className={cn(
        "pointer-events-none absolute inset-0 rounded-[inherit] [border:calc(var(--border-width)*1px)_solid_transparent]",

        // mask styles
        "![mask-clip:padding-box,border-box] ![mask-composite:intersect] [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]",

        // pseudo styles
        "after:absolute after:aspect-square after:w-[calc(var(--size)*1px)] after:animate-border-beam after:[animation-delay:var(--delay)] after:[background:linear-gradient(to_left,var(--color-from),var(--color-to),transparent)] after:[offset-anchor:calc(var(--anchor)*1%)_50%] after:[offset-path:rect(0_auto_auto_0_round_calc(var(--size)*1px))]",
        className
      )}
    />
  );
};



================================================
FILE: frontend/components/ui/button.tsx
================================================
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";

const buttonVariants = cva(
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default:
          "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
        outline:
          "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "h-9 w-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button";
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    );
  }
);
Button.displayName = "Button";

export { Button, buttonVariants };



================================================
FILE: frontend/components/ui/label.tsx
================================================
"use client"

import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const labelVariants = cva(
  "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)

const Label = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
    VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
  <LabelPrimitive.Root
    ref={ref}
    className={cn(labelVariants(), className)}
    {...props}
  />
))
Label.displayName = LabelPrimitive.Root.displayName

export { Label }



================================================
FILE: frontend/components/ui/popover.tsx
================================================
"use client";

import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";

import { cn } from "@/lib/utils";

const Popover = PopoverPrimitive.Root;

const PopoverTrigger = PopoverPrimitive.Trigger;

const PopoverAnchor = PopoverPrimitive.Anchor;

const PopoverContent = React.forwardRef<
  React.ElementRef<typeof PopoverPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
  <PopoverPrimitive.Portal>
    <PopoverPrimitive.Content
      ref={ref}
      align={align}
      sideOffset={sideOffset}
      className={cn(
        "z-50 w-72 rounded-md border bg-popover p-2 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
        className
      )}
      {...props}
    />
  </PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;

export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };



================================================
FILE: frontend/components/ui/select.tsx
================================================
"use client";

import * as React from "react";
import { ChevronDownIcon, ChevronUpIcon } from "@radix-ui/react-icons";
import * as SelectPrimitive from "@radix-ui/react-select";

import { cn } from "@/lib/utils";
import { CaretDown, Check } from "@phosphor-icons/react/dist/ssr";

const Select = SelectPrimitive.Root;

const SelectGroup = SelectPrimitive.Group;

const SelectValue = SelectPrimitive.Value;

const SelectTrigger = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Trigger
    ref={ref}
    className={cn(
      "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
      className
    )}
    {...props}
  >
    {children}
    <SelectPrimitive.Icon asChild>
      <CaretDown className="h-4 w-4 opacity-50" weight="bold" />
    </SelectPrimitive.Icon>
  </SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;

const SelectScrollUpButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.ScrollUpButton
    ref={ref}
    className={cn(
      "flex cursor-default items-center justify-center py-1",
      className
    )}
    {...props}
  >
    <ChevronUpIcon />
  </SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;

const SelectScrollDownButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.ScrollDownButton
    ref={ref}
    className={cn(
      "flex cursor-default items-center justify-center py-1",
      className
    )}
    {...props}
  >
    <ChevronDownIcon />
  </SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
  SelectPrimitive.ScrollDownButton.displayName;

const SelectContent = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
  <SelectPrimitive.Portal>
    <SelectPrimitive.Content
      ref={ref}
      className={cn(
        "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
        position === "popper" &&
          "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
        className
      )}
      position={position}
      {...props}
    >
      <SelectScrollUpButton />
      <SelectPrimitive.Viewport
        className={cn(
          "p-1",
          position === "popper" &&
            "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
        )}
      >
        {children}
      </SelectPrimitive.Viewport>
      <SelectScrollDownButton />
    </SelectPrimitive.Content>
  </SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;

const SelectLabel = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Label>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Label
    ref={ref}
    className={cn("px-2 py-1.5 text-sm font-semibold", className)}
    {...props}
  />
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;

const SelectItem = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Item
    ref={ref}
    className={cn(
      "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
      className
    )}
    {...props}
  >
    <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
      <SelectPrimitive.ItemIndicator>
        <Check weight="bold" className="h-4 w-4" />
      </SelectPrimitive.ItemIndicator>
    </span>
    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
  </SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;

const SelectSeparator = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Separator
    ref={ref}
    className={cn("-mx-1 my-1 h-px bg-muted", className)}
    {...props}
  />
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;

export {
  Select,
  SelectGroup,
  SelectValue,
  SelectTrigger,
  SelectContent,
  SelectLabel,
  SelectItem,
  SelectSeparator,
  SelectScrollUpButton,
  SelectScrollDownButton,
};



================================================
FILE: frontend/components/ui/switch.tsx
================================================
"use client";

import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";

import { cn } from "@/lib/utils";

const Switch = React.forwardRef<
  React.ElementRef<typeof SwitchPrimitives.Root>,
  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
  <SwitchPrimitives.Root
    className={cn(
      "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-blue-500 data-[state=unchecked]:bg-input",
      className
    )}
    {...props}
    ref={ref}
  >
    <SwitchPrimitives.Thumb
      className={cn(
        "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
      )}
    />
  </SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;

export { Switch };



================================================
FILE: frontend/components/ui/tooltip.tsx
================================================
"use client"

import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"

import { cn } from "@/lib/utils"

const TooltipProvider = TooltipPrimitive.Provider

const Tooltip = TooltipPrimitive.Root

const TooltipTrigger = TooltipPrimitive.Trigger

const TooltipContent = React.forwardRef<
  React.ElementRef<typeof TooltipPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
  <TooltipPrimitive.Content
    ref={ref}
    sideOffset={sideOffset}
    className={cn(
      "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
      className
    )}
    {...props}
  />
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName

export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }



================================================
FILE: frontend/lib/config.ts
================================================
export const SECOND_WIDTH = 200;



================================================
FILE: frontend/lib/utils.ts
================================================
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}


