Reset
1
00-Lesson-Site/.gitignore
vendored
@ -1 +0,0 @@
|
||||
.env
|
||||
@ -1,22 +0,0 @@
|
||||
services:
|
||||
leafpig-lesson-site-frontend-dev:
|
||||
container_name: leafpig-lesson-site-frontend-dev
|
||||
build:
|
||||
context: ./frontend
|
||||
target: development
|
||||
ports:
|
||||
- ${SERVER_FRONTEND_DEV_PORT:?error}:4321
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
profiles:
|
||||
- dev
|
||||
leafpig-lesson-site-frontend-prod:
|
||||
container_name: leafpig-lesson-site-frontend-prod
|
||||
build:
|
||||
context: ./frontend
|
||||
target: production
|
||||
ports:
|
||||
- ${SERVER_FRONTEND_PORT:?error}:5000
|
||||
profiles:
|
||||
- prod
|
||||
@ -1 +0,0 @@
|
||||
export default new Map();
|
||||
@ -1,4 +0,0 @@
|
||||
|
||||
export default new Map([
|
||||
["src/content/lessons/01-intro.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Flessons%2F01-intro.mdx&astroContentModuleFlag=true")]]);
|
||||
|
||||
218
00-Lesson-Site/frontend/.astro/content.d.ts
vendored
@ -1,218 +0,0 @@
|
||||
declare module 'astro:content' {
|
||||
interface Render {
|
||||
'.mdx': Promise<{
|
||||
Content: import('astro').MDXContent;
|
||||
headings: import('astro').MarkdownHeading[];
|
||||
remarkPluginFrontmatter: Record<string, any>;
|
||||
components: import('astro').MDXInstance<{}>['components'];
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'astro:content' {
|
||||
export interface RenderResult {
|
||||
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
|
||||
headings: import('astro').MarkdownHeading[];
|
||||
remarkPluginFrontmatter: Record<string, any>;
|
||||
}
|
||||
interface Render {
|
||||
'.md': Promise<RenderResult>;
|
||||
}
|
||||
|
||||
export interface RenderedContent {
|
||||
html: string;
|
||||
metadata?: {
|
||||
imagePaths: Array<string>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'astro:content' {
|
||||
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
|
||||
|
||||
export type CollectionKey = keyof AnyEntryMap;
|
||||
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
|
||||
|
||||
export type ContentCollectionKey = keyof ContentEntryMap;
|
||||
export type DataCollectionKey = keyof DataEntryMap;
|
||||
|
||||
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
|
||||
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
|
||||
ContentEntryMap[C]
|
||||
>['slug'];
|
||||
|
||||
export type ReferenceDataEntry<
|
||||
C extends CollectionKey,
|
||||
E extends keyof DataEntryMap[C] = string,
|
||||
> = {
|
||||
collection: C;
|
||||
id: E;
|
||||
};
|
||||
export type ReferenceContentEntry<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}) = string,
|
||||
> = {
|
||||
collection: C;
|
||||
slug: E;
|
||||
};
|
||||
export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
|
||||
collection: C;
|
||||
id: string;
|
||||
};
|
||||
|
||||
/** @deprecated Use `getEntry` instead. */
|
||||
export function getEntryBySlug<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
// Note that this has to accept a regular string too, for SSR
|
||||
entrySlug: E,
|
||||
): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
|
||||
/** @deprecated Use `getEntry` instead. */
|
||||
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
|
||||
collection: C,
|
||||
entryId: E,
|
||||
): Promise<CollectionEntry<C>>;
|
||||
|
||||
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
|
||||
collection: C,
|
||||
filter?: (entry: CollectionEntry<C>) => entry is E,
|
||||
): Promise<E[]>;
|
||||
export function getCollection<C extends keyof AnyEntryMap>(
|
||||
collection: C,
|
||||
filter?: (entry: CollectionEntry<C>) => unknown,
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
|
||||
export function getLiveCollection<C extends keyof LiveContentConfig['collections']>(
|
||||
collection: C,
|
||||
filter?: LiveLoaderCollectionFilterType<C>,
|
||||
): Promise<
|
||||
import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>
|
||||
>;
|
||||
|
||||
export function getEntry<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(
|
||||
entry: ReferenceContentEntry<C, E>,
|
||||
): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof DataEntryMap,
|
||||
E extends keyof DataEntryMap[C] | (string & {}),
|
||||
>(
|
||||
entry: ReferenceDataEntry<C, E>,
|
||||
): E extends keyof DataEntryMap[C]
|
||||
? Promise<DataEntryMap[C][E]>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
slug: E,
|
||||
): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof DataEntryMap,
|
||||
E extends keyof DataEntryMap[C] | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
id: E,
|
||||
): E extends keyof DataEntryMap[C]
|
||||
? string extends keyof DataEntryMap[C]
|
||||
? Promise<DataEntryMap[C][E]> | undefined
|
||||
: Promise<DataEntryMap[C][E]>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getLiveEntry<C extends keyof LiveContentConfig['collections']>(
|
||||
collection: C,
|
||||
filter: string | LiveLoaderEntryFilterType<C>,
|
||||
): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>;
|
||||
|
||||
/** Resolve an array of entry references from the same collection */
|
||||
export function getEntries<C extends keyof ContentEntryMap>(
|
||||
entries: ReferenceContentEntry<C, ValidContentEntrySlug<C>>[],
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
export function getEntries<C extends keyof DataEntryMap>(
|
||||
entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
|
||||
export function render<C extends keyof AnyEntryMap>(
|
||||
entry: AnyEntryMap[C][string],
|
||||
): Promise<RenderResult>;
|
||||
|
||||
export function reference<C extends keyof AnyEntryMap>(
|
||||
collection: C,
|
||||
): import('astro/zod').ZodEffects<
|
||||
import('astro/zod').ZodString,
|
||||
C extends keyof ContentEntryMap
|
||||
? ReferenceContentEntry<C, ValidContentEntrySlug<C>>
|
||||
: ReferenceDataEntry<C, keyof DataEntryMap[C]>
|
||||
>;
|
||||
// Allow generic `string` to avoid excessive type errors in the config
|
||||
// if `dev` is not running to update as you edit.
|
||||
// Invalid collection names will be caught at build time.
|
||||
export function reference<C extends string>(
|
||||
collection: C,
|
||||
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
|
||||
|
||||
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
|
||||
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
|
||||
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
|
||||
>;
|
||||
|
||||
type ContentEntryMap = {
|
||||
|
||||
};
|
||||
|
||||
type DataEntryMap = {
|
||||
"lessons": Record<string, {
|
||||
id: string;
|
||||
body?: string;
|
||||
collection: "lessons";
|
||||
data: any;
|
||||
rendered?: RenderedContent;
|
||||
filePath?: string;
|
||||
}>;
|
||||
|
||||
};
|
||||
|
||||
type AnyEntryMap = ContentEntryMap & DataEntryMap;
|
||||
|
||||
type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader<
|
||||
infer TData,
|
||||
infer TEntryFilter,
|
||||
infer TCollectionFilter,
|
||||
infer TError
|
||||
>
|
||||
? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
|
||||
: { data: never; entryFilter: never; collectionFilter: never; error: never };
|
||||
type ExtractDataType<T> = ExtractLoaderTypes<T>['data'];
|
||||
type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
|
||||
type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];
|
||||
type ExtractErrorType<T> = ExtractLoaderTypes<T>['error'];
|
||||
|
||||
type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> =
|
||||
LiveContentConfig['collections'][C]['schema'] extends undefined
|
||||
? ExtractDataType<LiveContentConfig['collections'][C]['loader']>
|
||||
: import('astro/zod').infer<
|
||||
Exclude<LiveContentConfig['collections'][C]['schema'], undefined>
|
||||
>;
|
||||
type LiveLoaderEntryFilterType<C extends keyof LiveContentConfig['collections']> =
|
||||
ExtractEntryFilterType<LiveContentConfig['collections'][C]['loader']>;
|
||||
type LiveLoaderCollectionFilterType<C extends keyof LiveContentConfig['collections']> =
|
||||
ExtractCollectionFilterType<LiveContentConfig['collections'][C]['loader']>;
|
||||
type LiveLoaderErrorType<C extends keyof LiveContentConfig['collections']> = ExtractErrorType<
|
||||
LiveContentConfig['collections'][C]['loader']
|
||||
>;
|
||||
|
||||
export type ContentConfig = typeof import("../src/content.config.mjs");
|
||||
export type LiveContentConfig = never;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1765150810247
|
||||
}
|
||||
}
|
||||
1
00-Lesson-Site/frontend/.astro/types.d.ts
vendored
@ -1 +0,0 @@
|
||||
/// <reference types="astro/client" />
|
||||
24
00-Lesson-Site/frontend/.gitignore
vendored
@ -1,24 +0,0 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
@ -1,43 +0,0 @@
|
||||
# Base Image
|
||||
FROM node:22-alpine AS base
|
||||
WORKDIR /app
|
||||
|
||||
# Enable pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
# Copy manifest files first to cache dependencies
|
||||
COPY pnpm-lock.yaml package.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install
|
||||
|
||||
# --- Development Stage ---
|
||||
FROM base AS development
|
||||
COPY . .
|
||||
# Astro default port
|
||||
EXPOSE 4321
|
||||
# --host is required to expose the server to the container
|
||||
CMD ["pnpm", "dev", "--host"]
|
||||
|
||||
# --- Build Stage ---
|
||||
FROM base AS build
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
# --- Production Stage ---
|
||||
FROM base AS production
|
||||
WORKDIR /app
|
||||
|
||||
# We install 'serve' globally here so we don't rely on node_modules
|
||||
# This keeps the final image smaller/cleaner
|
||||
RUN npm install -g serve
|
||||
|
||||
# Copy the built output from the build stage
|
||||
# Astro outputs to 'dist' by default
|
||||
COPY --from=build /app/dist ./dist
|
||||
COPY serve.json ./dist
|
||||
|
||||
# Expose the port you want for production (e.g., 3000)
|
||||
EXPOSE 5000
|
||||
|
||||
CMD ["serve", "dist", "-l", "5000", "--config", "serve.json"]
|
||||
@ -1 +0,0 @@
|
||||
#
|
||||
@ -1,31 +0,0 @@
|
||||
// @ts-check
|
||||
import mdx from "@astrojs/mdx";
|
||||
import solidJs from "@astrojs/solid-js";
|
||||
import expressiveCode from "astro-expressive-code";
|
||||
import { defineConfig } from "astro/config";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
solidJs(),
|
||||
expressiveCode({
|
||||
themes: ["vitesse-dark"],
|
||||
}),
|
||||
mdx(),
|
||||
],
|
||||
vite: {
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: `@use "/src/styles/global_vars" as *; \n @use "/src/styles/reset" as *; \n @use "/src/styles/global_fonts" as *; \n`,
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
port: 5000,
|
||||
strictPort: true,
|
||||
allowedHosts: ["leafpig.mangopig.tech"], //Remember to add domain here if deploying
|
||||
},
|
||||
},
|
||||
});
|
||||
6472
00-Lesson-Site/frontend/package-lock.json
generated
@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "web-dev",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.12",
|
||||
"@astrojs/solid-js": "^5.1.3",
|
||||
"astro": "^5.16.4",
|
||||
"astro-expressive-code": "^0.41.3",
|
||||
"serve": "^14.2.5",
|
||||
"solid-js": "^1.9.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass": "^1.94.2"
|
||||
}
|
||||
}
|
||||
5133
00-Lesson-Site/frontend/pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB |
@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,41 +0,0 @@
|
||||
{
|
||||
"name": "App",
|
||||
"icons": [
|
||||
{
|
||||
"src": "\/android-icon-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image\/png",
|
||||
"density": "0.75"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image\/png",
|
||||
"density": "1.0"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image\/png",
|
||||
"density": "1.5"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image\/png",
|
||||
"density": "2.0"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image\/png",
|
||||
"density": "3.0"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image\/png",
|
||||
"density": "4.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
@ -1,40 +0,0 @@
|
||||
{
|
||||
"headers": [
|
||||
{
|
||||
"source": "/_astro/**",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=31536000, immutable"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/fonts/**",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=31536000, immutable"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "**/*.@(html|json)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "no-cache, no-store, must-revalidate"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "**",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=0, must-revalidate"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
---
|
||||
// Path: src/components/Navbar/DarkModeToggle.astro
|
||||
import Sun from "../SVGs/Sun.astro";
|
||||
import Moon from "../SVGs/Moon.astro";
|
||||
import styles from "./DarkModeToggle.module.scss";
|
||||
---
|
||||
|
||||
<button id="theme-toggle" aria-label="Toggle Dark Mode" class={styles["toggle-btn"]}>
|
||||
<div class={styles["icon-container"]}>
|
||||
<span class={`${styles["color-icon"]} ${styles["moon-wrapper"]}`}>
|
||||
<Moon />
|
||||
</span>
|
||||
<span class={`${styles["color-icon"]} ${styles["sun-wrapper"]}`}>
|
||||
<Sun />
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
const handleToggleClick = () => {
|
||||
const root = document.documentElement;
|
||||
const isDark = root.classList.contains("dark");
|
||||
const targetTheme = isDark ? "light" : "dark";
|
||||
|
||||
if (targetTheme === "dark") {
|
||||
root.classList.add("dark");
|
||||
root.setAttribute("data-theme", "dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
root.setAttribute("data-theme", "light");
|
||||
}
|
||||
|
||||
localStorage.setItem("theme", targetTheme);
|
||||
};
|
||||
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
const toggleBtn = document.getElementById("theme-toggle");
|
||||
|
||||
if (toggleBtn) {
|
||||
toggleBtn.removeEventListener("click", handleToggleClick);
|
||||
toggleBtn.addEventListener("click", handleToggleClick);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -1,64 +0,0 @@
|
||||
/* Path: src/components/Navbar/DarkModeToggle.module.scss */
|
||||
|
||||
.toggle-btn {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
padding: 0;
|
||||
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
transition: background-color 500ms;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
position: relative;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.color-icon {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition:
|
||||
transform 1000ms ease,
|
||||
opacity 500ms;
|
||||
}
|
||||
|
||||
.moon-wrapper {
|
||||
transform: rotate(90deg);
|
||||
opacity: 0;
|
||||
|
||||
:global([data-theme="dark"]) & {
|
||||
transform: rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.sun-wrapper {
|
||||
transform: rotate(0deg);
|
||||
opacity: 1;
|
||||
|
||||
:global([data-theme="dark"]) & {
|
||||
transform: rotate(-90deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
---
|
||||
// Path: src/components/Navbar/Navbar.astro
|
||||
|
||||
import "../../styles/main.scss";
|
||||
import styles from "./Navbar.module.scss";
|
||||
|
||||
import DarkModeToggle from "./DarkModeToggle.astro";
|
||||
import UserIcon from "./UserIcon.astro";
|
||||
|
||||
const pathname = new URL(Astro.request.url).pathname;
|
||||
const isActive = pathname === "/" || pathname === "";
|
||||
---
|
||||
|
||||
<nav class={styles.navbar}>
|
||||
<a href="/" class:list={[styles["nav-logo"], { [styles["active"]]: isActive }]}> LeafPig </a>
|
||||
|
||||
<ul class={styles["nav-links"]}>
|
||||
<li>
|
||||
<a href="/lessons" class:list={[{ [styles["active"]]: pathname.startsWith("/lessons") }]}>
|
||||
Lessons
|
||||
{pathname.startsWith("/lessons") && <span class={styles["magic-line"]} transition:name="nav-underline" />}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/changelog" class:list={[{ [styles["active"]]: pathname.startsWith("/changelog") }]}>
|
||||
Changelog
|
||||
{pathname.startsWith("/changelog") && <span class={styles["magic-line"]} transition:name="nav-underline" />}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/resources" class:list={[{ [styles["active"]]: pathname.startsWith("/resources") }]}>
|
||||
Resources
|
||||
{pathname.startsWith("/resources") && <span class={styles["magic-line"]} transition:name="nav-underline" />}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class={styles["nav-right"]} transition:persist="nav-tools">
|
||||
<DarkModeToggle />
|
||||
<UserIcon />
|
||||
</div>
|
||||
</nav>
|
||||
@ -1,97 +0,0 @@
|
||||
/* Path: src/components/Navbar/Navbar.module.scss */
|
||||
|
||||
.navbar {
|
||||
width: 100%;
|
||||
height: 6rem;
|
||||
padding: 0.75rem 2rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
font-size: 1.5rem;
|
||||
transition:
|
||||
transform 1000ms ease,
|
||||
opacity 500ms;
|
||||
|
||||
color: color-adjust(text, 0, 0);
|
||||
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
|
||||
margin-right: auto;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
background-color: color-adjust(text, 0, 0);
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -4px;
|
||||
transform-origin: left;
|
||||
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
&:hover::after,
|
||||
&:focus::after,
|
||||
&.active::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
margin-right: 2rem;
|
||||
justify-items: end;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
margin-left: 2rem;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: color-adjust(text, 0, 0);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
|
||||
transition: color 300ms ease-in-out;
|
||||
|
||||
&.active {
|
||||
color: color-adjust(secondary, 0, 0);
|
||||
}
|
||||
|
||||
.magic-line {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -4px;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: color-adjust(secondary, 0, 0);
|
||||
z-index: 10;
|
||||
contain: layout;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
---
|
||||
// Path: src/components/Navbar/UserIcon.astro
|
||||
|
||||
import ProfileSpinner from "../SVGs/ProfileSpinner.astro";
|
||||
import UserSVG from "../SVGs/UserSVG.astro";
|
||||
import styles from "./UserIcon.module.scss";
|
||||
---
|
||||
|
||||
<a class={styles["user-icon"]}>
|
||||
<div class={styles["spin-container"]}>
|
||||
<div class={styles["spin-animation"]}>
|
||||
<ProfileSpinner />
|
||||
</div>
|
||||
</div>
|
||||
<UserSVG />
|
||||
</a>
|
||||
@ -1,65 +0,0 @@
|
||||
/* Path: src/components/Navbar/UserIcon.module.scss */
|
||||
|
||||
.user-icon {
|
||||
display: inline-flex;
|
||||
// 1. Fix alignment (Flexbox uses justify-content)
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
// 2. Create a positioning context for the absolute child
|
||||
position: relative;
|
||||
|
||||
margin-left: 1rem;
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
border-radius: 50%;
|
||||
|
||||
// Optional: Reset link styles
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// 3. Trigger the animation when the USER hovers the main button
|
||||
&:hover .spin-container {
|
||||
animation-play-state: running;
|
||||
}
|
||||
}
|
||||
|
||||
.spin-container {
|
||||
position: absolute;
|
||||
// 4. Force the container to fill the parent exactly
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// Animation 1: Rotate CCW fast
|
||||
animation: spin 1.5s ease-in-out infinite reverse;
|
||||
animation-play-state: paused;
|
||||
|
||||
// Allow clicks to pass through to the link/button underneath
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spin-animation {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
animation: spin 15s linear infinite normal;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
---
|
||||
// Path: src/components/Post/Blockquotes/Ganbatte.astro
|
||||
|
||||
import styles from "./Ganbatte.module.scss";
|
||||
|
||||
interface Props {
|
||||
toc?: string;
|
||||
tocLevel?: string;
|
||||
imageAlt?: string;
|
||||
}
|
||||
|
||||
const { toc, tocLevel = "1", imageAlt = "MangoPig Ganbatte" } = Astro.props;
|
||||
---
|
||||
|
||||
<blockquote class={styles.ganbatte} data-toc={toc} data-toc-level={tocLevel}>
|
||||
<slot />
|
||||
|
||||
<picture>
|
||||
<img src="https://pic.mangopig.tech/i/4c4d1b5f-b9ce-4952-a1b4-991b19c0adb5.png" alt={imageAlt} />
|
||||
</picture>
|
||||
</blockquote>
|
||||
@ -1,41 +0,0 @@
|
||||
/* Path: src/components/Post/Blockquotes/Ganbatte.module.scss */
|
||||
|
||||
.ganbatte {
|
||||
background-color: #fff45e1a;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
|
||||
picture {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
|
||||
margin: 0;
|
||||
|
||||
width: 200px;
|
||||
max-width: 30%;
|
||||
|
||||
transform: rotate(10deg);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 20px;
|
||||
margin-right: 220px;
|
||||
}
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 30px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
---
|
||||
// Path: frontend/src/components/Post/Blockquotes/Homework.astro
|
||||
|
||||
import styles from "./Homework.module.scss";
|
||||
|
||||
interface Props {
|
||||
toc?: string;
|
||||
tocLevel?: string;
|
||||
imageAlt?: string;
|
||||
}
|
||||
|
||||
const { toc = "Homework", tocLevel = "1", imageAlt = "MangoPig Homework" } = Astro.props;
|
||||
---
|
||||
|
||||
<blockquote class={styles.homework} data-toc={toc} data-toc-level={tocLevel}>
|
||||
<slot />
|
||||
|
||||
<picture>
|
||||
<img src="https://pic.mangopig.tech/i/ce28cb80-5190-4fb3-b193-8b082cb326d8.webp" alt={imageAlt} />
|
||||
</picture>
|
||||
</blockquote>
|
||||
@ -1,41 +0,0 @@
|
||||
/* Path: frontend/src/components/Post/Blockquotes/Homework.module.scss */
|
||||
|
||||
.homework {
|
||||
background-color: #a95eff1a;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
|
||||
picture {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
|
||||
margin: 0;
|
||||
|
||||
width: 200px;
|
||||
max-width: 30%;
|
||||
|
||||
transform: rotate(10deg);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 20px;
|
||||
margin-right: 220px;
|
||||
}
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 30px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
---
|
||||
// Path: src/components/Post/Blockquotes/Important.astro
|
||||
|
||||
import styles from "./Important.module.scss";
|
||||
---
|
||||
|
||||
<blockquote class={styles.important}>
|
||||
<slot />
|
||||
<picture class={styles.sticker}>
|
||||
<img src="https://pic.mangopig.tech/i/7eb5b343-5ddf-47ae-a272-4b82ca3d53d7.webp" alt="MangoPig Important" />
|
||||
</picture>
|
||||
</blockquote>
|
||||
@ -1,55 +0,0 @@
|
||||
/* Path: src/components/Post/Blockquotes/Important.module.scss */
|
||||
|
||||
.important {
|
||||
background-color: #ff5e5e33;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
.sticker {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: -10px;
|
||||
|
||||
margin: 0;
|
||||
|
||||
width: 100px;
|
||||
max-width: 30%;
|
||||
|
||||
transform: rotate(10deg);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 20px;
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
margin-top: 20px;
|
||||
padding-left: 20px;
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
span {
|
||||
// Place in middle vertically
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 30px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
---
|
||||
// Path: src/components/Post/Blockquotes/Info.astro
|
||||
|
||||
import styles from "./Info.module.scss";
|
||||
---
|
||||
|
||||
<blockquote class={styles.info}>
|
||||
<slot />
|
||||
<picture class={styles.sticker}>
|
||||
<img src="https://pic.mangopig.tech/i/ebf2e26a-8791-4277-90cb-079ad7454aef.webp" alt="MangoPig Ganbattte" />
|
||||
</picture>
|
||||
</blockquote>
|
||||
@ -1,53 +0,0 @@
|
||||
/* Path: src/components/Post/Blockquotes/Info.module.scss */
|
||||
|
||||
.info {
|
||||
background-color: #5efaff1a;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
|
||||
.sticker {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
|
||||
margin: 0;
|
||||
|
||||
width: 100px;
|
||||
max-width: 30%;
|
||||
|
||||
transform: rotate(10deg);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 20px;
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
margin-top: 20px;
|
||||
padding-left: 20px;
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
span {
|
||||
// Place in middle vertically
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 30px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
---
|
||||
// Path: src/components/Post/Blockquotes/QA.astro
|
||||
import styles from "./QA.module.scss";
|
||||
---
|
||||
|
||||
<div class={styles.qaContainer}>
|
||||
{/* The Question Section */}
|
||||
<div class={styles.questionHeader}>
|
||||
<span class={styles.prefix}>Q:</span>
|
||||
<span class={styles.questionText}>
|
||||
<slot name="question" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* The Answer Section (Default Slot) */}
|
||||
<div class={styles.answerBody}>
|
||||
<span class={styles.prefix}>A:</span>
|
||||
<div class={styles.answerContent}>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* The Sticker */}
|
||||
<picture class={styles.sticker}>
|
||||
<img src="https://pic.mangopig.tech/i/4c4d1b5f-b9ce-4952-a1b4-991b19c0adb5.png" alt="Thinking MangoPig" />
|
||||
</picture>
|
||||
</div>
|
||||
@ -1,78 +0,0 @@
|
||||
/* Path: src/components/Post/Blockquotes/QA.module.scss */
|
||||
|
||||
.qaContainer {
|
||||
// Use a yellowish tint to differentiate from Info block
|
||||
background-color: #fff45e26;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
min-height: 120px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
// --- Sticker Logic (Same as Info block) ---
|
||||
.sticker {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
margin: 0;
|
||||
width: 100px;
|
||||
max-width: 30%;
|
||||
// Rotate opposite way for variety
|
||||
transform: rotate(-10deg);
|
||||
pointer-events: none;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
//Common prefix style (Q: and A:)
|
||||
.prefix {
|
||||
font-weight: 800;
|
||||
color: #ffbd72; // Matches your H2 color scheme
|
||||
margin-right: 12px;
|
||||
display: inline-block;
|
||||
min-width: 25px;
|
||||
}
|
||||
|
||||
// --- Question Section ---
|
||||
.questionHeader {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.1em;
|
||||
font-weight: 700;
|
||||
// Ensure text doesn't hit the sticker
|
||||
margin-right: 90px;
|
||||
color: color-adjust(primary, 0, 0);
|
||||
}
|
||||
|
||||
// --- Answer Section ---
|
||||
.answerBody {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
// Ensure text doesn't hit the sticker
|
||||
margin-right: 90px;
|
||||
}
|
||||
|
||||
.answerContent {
|
||||
flex: 1;
|
||||
line-height: 1.6;
|
||||
|
||||
// Handle standard markdown elements inside the answer slot
|
||||
p {
|
||||
margin-bottom: 1em;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-bottom: 1em;
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
---
|
||||
// Path: src/components/Post/FloatingTOC.astro
|
||||
import styles from "./FloatingTOC.module.scss";
|
||||
---
|
||||
|
||||
<nav class={styles.toc} aria-label="Table of Contents">
|
||||
<ul id="toc-list"></ul>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
function generateTOC() {
|
||||
const content = document.getElementById("lesson-container");
|
||||
const list = document.getElementById("toc-list");
|
||||
|
||||
if (list) list.innerHTML = "";
|
||||
if (!content || !list) return;
|
||||
|
||||
const targets = content.querySelectorAll("[data-toc]");
|
||||
|
||||
targets.forEach((el, index) => {
|
||||
const label = el.getAttribute("data-toc") || "";
|
||||
const level = el.getAttribute("data-toc-level") || "1";
|
||||
|
||||
// --- CHANGED SECTION START: ID Generation (Slugify) ---
|
||||
if (!el.id) {
|
||||
// Convert "Setting Up Developer Environment" -> "setting-up-developer-environment"
|
||||
const slug = label
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, "") // Remove non-word chars
|
||||
.replace(/[\s_-]+/g, "-") // Replace spaces with dashes
|
||||
.replace(/^-+|-+$/g, ""); // Trim dashes from start/end
|
||||
|
||||
// Safety check: If ID exists or slug is empty, fallback to index
|
||||
if (!slug || document.getElementById(slug)) {
|
||||
el.id = `section-${index}`;
|
||||
} else {
|
||||
el.id = slug;
|
||||
}
|
||||
}
|
||||
// --- CHANGED SECTION END ---
|
||||
|
||||
const li = document.createElement("li");
|
||||
const a = document.createElement("a");
|
||||
|
||||
li.setAttribute("data-level", level);
|
||||
|
||||
a.href = `#${el.id}`;
|
||||
a.innerHTML = `<span class="toc-text">${label}</span>`;
|
||||
|
||||
a.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
// Optional: Update URL hash without jumping
|
||||
history.pushState(null, "", `#${el.id}`);
|
||||
});
|
||||
|
||||
li.appendChild(a);
|
||||
list.appendChild(li);
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
document.querySelectorAll("#toc-list a").forEach((link) => link.classList.remove("active"));
|
||||
a.classList.add("active");
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-50% 0px -50% 0px", threshold: 0 }
|
||||
);
|
||||
|
||||
observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
generateTOC();
|
||||
document.addEventListener("astro:after-swap", generateTOC);
|
||||
</script>
|
||||
@ -1,142 +0,0 @@
|
||||
/* Path: src/components/Post/FloatingTOC.module.scss */
|
||||
|
||||
.toc {
|
||||
// 1. Container Layout
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
right: 20px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 100;
|
||||
|
||||
// 2. THE FIX: Make container wide enough to hold the text
|
||||
width: 300px;
|
||||
|
||||
// 3. THE FIX: Allow clicks to pass through the empty area
|
||||
pointer-events: none;
|
||||
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
|
||||
// Keep scrolling functionality
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Nesting protections
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end; // Keep lines on the right
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%; // Ensure list item spans width
|
||||
}
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
text-decoration: none;
|
||||
padding: 5px 0;
|
||||
cursor: pointer;
|
||||
|
||||
// 4. THE FIX: Re-enable clicks on the actual links
|
||||
pointer-events: auto;
|
||||
|
||||
// --- THE LINE ---
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 3px;
|
||||
background-color: #ffb8b8;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
// --- THE TEXT ---
|
||||
:global(.toc-text) {
|
||||
position: absolute;
|
||||
|
||||
// 5. THE FIX: Position relative to the line inside the new wider box
|
||||
// Since the box is 300px wide, we don't need 'right: 150%'.
|
||||
// We just place it to the left of the line.
|
||||
right: 60px;
|
||||
|
||||
white-space: nowrap;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: color-adjust(text, 0, 0);
|
||||
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
transition: all 0.3s ease;
|
||||
pointer-events: none;
|
||||
|
||||
background: color-adjust(bg, 0, 0.8);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
// --- HOVER STATE ---
|
||||
&:hover {
|
||||
:global(.toc-text) {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
&::after {
|
||||
background-color: color-adjust(text, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
// --- ACTIVE STATE ---
|
||||
&:global(.active) {
|
||||
&::after {
|
||||
background-color: color-adjust(secondary, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- STAGGERED LENGTHS ---
|
||||
li[data-level="1"] {
|
||||
a::after {
|
||||
width: 40px;
|
||||
}
|
||||
a:global(.active)::after {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
li[data-level="2"] {
|
||||
a::after {
|
||||
width: 25px;
|
||||
}
|
||||
a:global(.active)::after {
|
||||
width: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
li[data-level="3"] {
|
||||
a::after {
|
||||
width: 15px;
|
||||
}
|
||||
a:global(.active)::after {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
/* Path: src/components/Post/Spoiler.module.scss */
|
||||
|
||||
.spoiler {
|
||||
border-left: 4px solid color-adjust(secondary, 0, 0);
|
||||
padding: 1rem;
|
||||
background: rgba(128, 128, 128, 0.1);
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: color-adjust(primary, 0, 0);
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spoilerContent {
|
||||
margin-top: 1rem;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
// Path: src/components/Post/Spoiler.tsx
|
||||
import type { Component, JSX } from "solid-js";
|
||||
import { createSignal } from "solid-js";
|
||||
import styles from "./Spoiler.module.scss";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
buttonText?: string;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
const Spoiler: Component<Props> = (props) => {
|
||||
const [visible, setVisible] = createSignal(false);
|
||||
|
||||
return (
|
||||
<blockquote class={styles.spoiler}>
|
||||
{props.title ? <strong class={styles.spoilerTitle}>{props.title}</strong> : <></>}
|
||||
<button onClick={() => setVisible(!visible())}>
|
||||
{visible() ? "Hide" : "Show"}
|
||||
{props.buttonText ? `${props.buttonText}` : " Answer"}
|
||||
</button>
|
||||
|
||||
{visible() && <div class={styles.spoilerContent}>{props.children}</div>}
|
||||
</blockquote>
|
||||
);
|
||||
};
|
||||
|
||||
export default Spoiler;
|
||||
@ -1,19 +0,0 @@
|
||||
---
|
||||
// Path: src/components/SVGs/Moon.astro
|
||||
---
|
||||
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" class="svg-icon" stroke-width={1}>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="currentColor"
|
||||
d="M14.228 7.9439C10.5176 8.82869 7.75757 12.1054 7.75757 15.9987C7.75757 20.5716 11.5618 24.2919 16.2367 24.2919C19.2323 24.2919 21.9337 22.7699 23.4514 20.3585C23.2779 20.3676 23.1033 20.3722 22.9287 20.3722C17.7826 20.3722 13.5951 16.2772 13.5951 11.2435C13.5951 10.1032 13.8108 8.98914 14.228 7.9439M16.2367 26.4993C10.3171 26.4993 5.50037 21.7899 5.50037 15.9987C5.50037 10.2109 10.3171 5.49927 16.2367 5.49927C16.6598 5.49927 17.0501 5.72963 17.2435 6.09753C17.438 6.46428 17.4087 6.90668 17.1638 7.24363C16.3059 8.42297 15.8535 9.80631 15.8535 11.2435C15.8535 15.06 19.0272 18.1637 22.9287 18.1637C23.6483 18.1637 24.3573 18.0582 25.0359 17.8531C25.4378 17.7293 25.8785 17.8359 26.1738 18.1304C26.4715 18.425 26.5758 18.8559 26.4446 19.2467C25.0019 23.5847 20.9 26.4993 16.2367 26.4993"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
<style lang="scss">
|
||||
.svg-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -1,9 +0,0 @@
|
||||
---
|
||||
// Path: src/components/SVGs/ProfileSpinner.astro
|
||||
---
|
||||
|
||||
<svg height={56} width={56} viewBox="0 0 56 56">
|
||||
<path d="M29.465,0.038373A28,28,0,0,1,52.948,40.712L51.166,39.804A26,26,0,0,0,29.361,2.0356Z" style="color: oklch(62.3% 0.214 259.815);" fill="currentColor"></path>
|
||||
<path d="M51.483,43.250A28,28,0,0,1,4.5172,43.250L6.1946,42.161A26,26,0,0,0,49.805,42.161Z" style="color: oklch(79.5% 0.184 86.047);" fill="currentColor"></path>
|
||||
<path d="M3.0518,40.712A28,28,0,0,1,26.535,0.038373L26.639,2.0356A26,26,0,0,0,4.8338,39.804Z" style="color: #D14242;" fill="currentColor"></path>
|
||||
</svg>
|
||||
@ -1,58 +0,0 @@
|
||||
---
|
||||
// Path: src/components/SVGs/Sun.astro
|
||||
---
|
||||
|
||||
<svg class="w-full" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" class="svg-icon" stroke-width={1}>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M16.0003 21.4194C13.0123 21.4194 10.5813 18.9874 10.5813 15.9994C10.5813 13.0114 13.0123 10.5804 16.0003 10.5804C18.9883 10.5804 21.4193 13.0114 21.4193 15.9994C21.4193 18.9874 18.9883 21.4194 16.0003 21.4194M16.0003 8.64136C11.9423 8.64136 8.64233 11.9414 8.64233 15.9994C8.64233 20.0574 11.9423 23.3574 16.0003 23.3574C20.0573 23.3574 23.3583 20.0574 23.3583 15.9994C23.3583 11.9414 20.0573 8.64136 16.0003 8.64136"
|
||||
fill="currentColor"></path>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.11559 15.0298H3.34559C2.81059 15.0298 2.37659 15.4648 2.37659 15.9998C2.37659 16.5348 2.81059 16.9688 3.34559 16.9688H6.11559C6.65159 16.9688 7.08459 16.5348 7.08459 15.9998C7.08459 15.4648 6.65159 15.0298 6.11559 15.0298"
|
||||
fill="currentColor"></path>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M16.0004 7.08447C16.5364 7.08447 16.9704 6.64946 16.9704 6.11446V3.34546C16.9704 2.81046 16.5364 2.37646 16.0004 2.37646C15.4644 2.37646 15.0304 2.81046 15.0304 3.34546V6.11446C15.0304 6.64946 15.4644 7.08447 16.0004 7.08447"
|
||||
fill="currentColor"></path>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M16.0004 24.9146C15.4644 24.9146 15.0304 25.3496 15.0304 25.8846V28.6536C15.0304 29.1886 15.4644 29.6236 16.0004 29.6236C16.5364 29.6236 16.9704 29.1886 16.9704 28.6536V25.8846C16.9704 25.3496 16.5364 24.9146 16.0004 24.9146"
|
||||
fill="currentColor"></path>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M28.6542 15.0298H25.8842C25.3492 15.0298 24.9152 15.4648 24.9152 15.9998C24.9152 16.5348 25.3492 16.9688 25.8842 16.9688H28.6542C29.1902 16.9688 29.6242 16.5348 29.6242 15.9998C29.6242 15.4648 29.1902 15.0298 28.6542 15.0298"
|
||||
fill="currentColor"></path>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M22.9896 9.97995C23.2376 9.97995 23.4856 9.88495 23.6756 9.69595L24.7036 8.66795C25.0816 8.28995 25.0816 7.67495 24.7036 7.29595C24.3246 6.91795 23.7106 6.91795 23.3316 7.29595L22.3036 8.32495C21.9256 8.70295 21.9256 9.31695 22.3036 9.69595C22.4926 9.88495 22.7416 9.97995 22.9896 9.97995"
|
||||
fill="currentColor"></path>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.32507 9.69593C8.51407 9.88493 8.76207 9.97993 9.01107 9.97993C9.25907 9.97993 9.50707 9.88493 9.69607 9.69593C10.0751 9.31693 10.0751 8.70293 9.69607 8.32493L8.66807 7.29693C8.28907 6.91893 7.67507 6.91893 7.29707 7.29693C6.91807 7.67493 6.91807 8.28993 7.29707 8.66793L8.32507 9.69593Z"
|
||||
fill="currentColor"></path>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.32507 22.3043L7.29707 23.3313C6.91807 23.7093 6.91807 24.3243 7.29707 24.7023C7.48607 24.8923 7.73407 24.9873 7.98207 24.9873C8.23007 24.9873 8.47807 24.8923 8.66807 24.7023L9.69607 23.6753C10.0751 23.2973 10.0751 22.6833 9.69607 22.3043C9.31807 21.9253 8.70307 21.9253 8.32507 22.3043"
|
||||
fill="currentColor"></path>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M23.6752 22.3043C23.2962 21.9253 22.6822 21.9253 22.3032 22.3043C21.9252 22.6833 21.9252 23.2973 22.3042 23.6753L23.3322 24.7023C23.5212 24.8923 23.7692 24.9873 24.0182 24.9873C24.2662 24.9873 24.5142 24.8923 24.7032 24.7023C25.0822 24.3243 25.0822 23.7093 24.7032 23.3313L23.6752 22.3043Z"
|
||||
fill="currentColor"></path>
|
||||
</svg>
|
||||
|
||||
<style lang="scss">
|
||||
.svg-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -1,7 +0,0 @@
|
||||
---
|
||||
// Path: src/components/SVGs/UserSVG.astro
|
||||
---
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="width: 2rem; height: 2rem" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width={1.5}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
@ -1,169 +0,0 @@
|
||||
/* Path: 00-Lesson-Site/frontend/src/components/Util/QuantizationCalc.module.scss */
|
||||
|
||||
@use "../../styles/global_vars" as *;
|
||||
|
||||
.wrapper {
|
||||
font-family: "Geist", sans-serif;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
|
||||
// Background: Base + slight tint (0.02)
|
||||
background-color: color-adjust(background, 0.02, 0);
|
||||
|
||||
// Border: Background + higher contrast (0.1)
|
||||
border: 1px solid color-adjust(background, 0.1, 0);
|
||||
|
||||
// Shadow: kept as rgba for transparency, or could be replaced if you have a shadow mixin
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
|
||||
color: color-adjust(text, 0, 0);
|
||||
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.5rem;
|
||||
color: color-adjust(text, 0, 0);
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.inputGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
// Slightly softer text than title
|
||||
color: color-adjust(text, -0.05, 0);
|
||||
}
|
||||
|
||||
input[type="number"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
|
||||
// Border: Background + medium contrast
|
||||
border: 1px solid color-adjust(background, 0.15, 0);
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
|
||||
// Input BG: Pure base background
|
||||
background-color: color-adjust(background, 0, 0);
|
||||
color: color-adjust(text, 0, 0);
|
||||
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: color-adjust(primary, 0, 0);
|
||||
// using primary color for the ring with opacity if supported,
|
||||
// otherwise fallback to raw color or keeping the rgba hardcoded for alpha
|
||||
box-shadow: 0 0 0 2px color-adjust(primary, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkboxGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
cursor: pointer;
|
||||
accent-color: color-adjust(primary, 0, 0);
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
// Softer text for label
|
||||
color: color-adjust(text, -0.1, 0);
|
||||
font-size: 0.95rem;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.resultBox {
|
||||
// Result box uses pure base background (white in light mode)
|
||||
// to stand out from the slightly tinted wrapper
|
||||
background: color-adjust(background, 0, 0);
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid color-adjust(background, 0.1, 0);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.resultHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
// Secondary text color
|
||||
color: color-adjust(text, -0.2, 0);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
// Highlight with primary color
|
||||
color: color-adjust(primary, 0, 0);
|
||||
}
|
||||
|
||||
.subtext {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
// Muted text color
|
||||
color: color-adjust(text, -0.3, 0);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.equationBox {
|
||||
background: color-adjust(background, 0.08, 0);
|
||||
|
||||
color: color-adjust(text, 0, 0);
|
||||
padding: 1.25rem;
|
||||
border-radius: 8px;
|
||||
font-family: "GeistMono", monospace;
|
||||
font-size: 0.9rem;
|
||||
overflow-x: auto;
|
||||
line-height: 1.6;
|
||||
border: 1px solid color-adjust(background, 0.15, 0);
|
||||
}
|
||||
|
||||
.eqTitle {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: bold;
|
||||
color: color-adjust(text, -0.3, 0);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.eqMath {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.eqResult {
|
||||
margin-top: 0.5rem;
|
||||
color: color-adjust(primary, 0, 0);
|
||||
font-weight: bold;
|
||||
}
|
||||
@ -1,125 +0,0 @@
|
||||
// Path: 00-Lesson-Site/frontend/src/components/Util/QuantizationCalc.tsx
|
||||
|
||||
import { createMemo, createSignal, For, type Component } from "solid-js";
|
||||
import styles from "./QuantizationCalc.module.scss";
|
||||
|
||||
type QuantMethod = {
|
||||
id: number;
|
||||
name: string;
|
||||
bpw: number;
|
||||
desc: string;
|
||||
};
|
||||
|
||||
// Data derived from the provided Llama-3-8B and IQ/TQ specs
|
||||
const QUANT_DATA: QuantMethod[] = [
|
||||
{ id: 0, name: "F32", bpw: 32.0, desc: "Standard Float 32 (Uncompressed)" },
|
||||
{ id: 32, name: "BF16 / F16", bpw: 16.0, desc: "Half Precision" },
|
||||
{ id: 7, name: "Q8_0", bpw: 7.96, desc: "Almost lossless" },
|
||||
{ id: 18, name: "Q6_K", bpw: 6.14, desc: "High quality" },
|
||||
{ id: 9, name: "Q5_1", bpw: 5.65, desc: "High accuracy" },
|
||||
{ id: 17, name: "Q5_K_M (Q5_K)", bpw: 5.33, desc: "Recommended balance" },
|
||||
{ id: 16, name: "Q5_K_S", bpw: 5.21, desc: "" },
|
||||
{ id: 8, name: "Q5_0", bpw: 5.21, desc: "Legacy standard" },
|
||||
{ id: 3, name: "Q4_1", bpw: 4.78, desc: "" },
|
||||
{ id: 15, name: "Q4_K_M (Q4_K)", bpw: 4.58, desc: "Most popular daily driver" },
|
||||
{ id: 25, name: "IQ4_NL", bpw: 4.5, desc: "Non-linear quantization" },
|
||||
{ id: 14, name: "Q4_K_S", bpw: 4.37, desc: "Fast inference" },
|
||||
{ id: 2, name: "Q4_0", bpw: 4.34, desc: "Very fast" },
|
||||
{ id: 30, name: "IQ4_XS", bpw: 4.25, desc: "" },
|
||||
{ id: 13, name: "Q3_K_L", bpw: 4.03, desc: "" },
|
||||
{ id: 12, name: "Q3_K_M (Q3_K)", bpw: 3.74, desc: "Decent for lower VRAM" },
|
||||
{ id: 27, name: "IQ3_M", bpw: 3.66, desc: "Mix quantization" },
|
||||
{ id: 26, name: "IQ3_S", bpw: 3.44, desc: "" },
|
||||
{ id: 11, name: "Q3_K_S", bpw: 3.41, desc: "" },
|
||||
{ id: 22, name: "IQ3_XS", bpw: 3.3, desc: "" },
|
||||
{ id: 21, name: "Q2_K_S", bpw: 3.18, desc: "Significant quality loss" },
|
||||
{ id: 23, name: "IQ3_XXS", bpw: 3.06, desc: "" },
|
||||
{ id: 10, name: "Q2_K", bpw: 2.96, desc: "Legacy 2-bit" },
|
||||
{ id: 29, name: "IQ2_M", bpw: 2.7, desc: "SOTA 2-bit" },
|
||||
{ id: 28, name: "IQ2_S", bpw: 2.5, desc: "" },
|
||||
{ id: 20, name: "IQ2_XS", bpw: 2.31, desc: "" },
|
||||
{ id: 19, name: "IQ2_XXS", bpw: 2.06, desc: "" },
|
||||
{ id: 37, name: "TQ2_0", bpw: 2.06, desc: "Ternarization" },
|
||||
{ id: 31, name: "IQ1_M", bpw: 1.75, desc: "Extreme compression" },
|
||||
{ id: 36, name: "TQ1_0", bpw: 1.69, desc: "Ternarization" },
|
||||
{ id: 24, name: "IQ1_S", bpw: 1.56, desc: "Experimental" },
|
||||
];
|
||||
|
||||
const QuantizationCalculator: Component = () => {
|
||||
const [params, setParams] = createSignal<number>(8);
|
||||
const [selectedQuantId, setSelectedQuantId] = createSignal<number>(15);
|
||||
const [includeOverhead, setIncludeOverhead] = createSignal<boolean>(true);
|
||||
|
||||
const selectedQuant = createMemo(() => QUANT_DATA.find((q) => q.id === selectedQuantId()) || QUANT_DATA[0]);
|
||||
|
||||
const modelSizeGB = createMemo(() => {
|
||||
return (params() * selectedQuant().bpw) / 8;
|
||||
});
|
||||
|
||||
const totalVramEstimation = createMemo(() => {
|
||||
const size = modelSizeGB();
|
||||
// +0.5GB CUDA Context + ~15% for KV Cache
|
||||
const overhead = includeOverhead() ? 0.5 + size * 0.15 : 0;
|
||||
return size + overhead;
|
||||
});
|
||||
|
||||
return (
|
||||
<div class={styles.wrapper}>
|
||||
<h2 class={styles.title}>LLM VRAM Calculator</h2>
|
||||
|
||||
{/* --- Inputs --- */}
|
||||
<div class={styles.controls}>
|
||||
{/* Parameter Input */}
|
||||
<div class={styles.inputGroup}>
|
||||
<label for="model-params">Model Parameters (Billions)</label>
|
||||
<input id="model-params" type="number" min="0.1" step="0.1" value={params()} onInput={(e) => setParams(parseFloat(e.currentTarget.value) || 0)} />
|
||||
</div>
|
||||
|
||||
{/* Quantization Select */}
|
||||
<div class={styles.inputGroup}>
|
||||
<label for="quant-method">Quantization Method</label>
|
||||
<select id="quant-method" value={selectedQuantId()} onChange={(e) => setSelectedQuantId(parseInt(e.currentTarget.value))}>
|
||||
<For each={QUANT_DATA}>
|
||||
{(quant) => (
|
||||
<option value={quant.id}>
|
||||
{quant.name} ({quant.bpw} bpw) {quant.desc ? `- ${quant.desc}` : ""}
|
||||
</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Overhead Toggle */}
|
||||
<div class={styles.checkboxGroup}>
|
||||
<input type="checkbox" id="overhead-check" checked={includeOverhead()} onChange={(e) => setIncludeOverhead(e.currentTarget.checked)} />
|
||||
<label for="overhead-check">Include Estimated Overhead (KV Cache + Context)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* --- Results --- */}
|
||||
<div class={styles.resultBox}>
|
||||
<div class={styles.resultHeader}>
|
||||
<span class={styles.label}>Estimated VRAM:</span>
|
||||
<span class={styles.value}>{totalVramEstimation().toFixed(2)} GB</span>
|
||||
</div>
|
||||
<p class={styles.subtext}>
|
||||
(Model Weights: {modelSizeGB().toFixed(2)} GB {includeOverhead() ? "+ Overhead" : ""})
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* --- Equation Display --- */}
|
||||
<div class={styles.equationBox}>
|
||||
<span class={styles.eqTitle}>Calculation Trace</span>
|
||||
<div class={styles.eqMath}>
|
||||
VRAM = ( {params()}B params × {selectedQuant().bpw} bpw ) / 8
|
||||
</div>
|
||||
<div class={styles.eqResult}>
|
||||
= {modelSizeGB().toFixed(4)} GB
|
||||
{includeOverhead() ? " + KV_Cache_Overhead" : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuantizationCalculator;
|
||||
@ -1,958 +0,0 @@
|
||||
---
|
||||
# Path: 00-Lesson-Site/frontend/src/content/lessons/01-intro.mdx
|
||||
|
||||
title: "Introduction to Web Dev"
|
||||
description: "Setting up the environment"
|
||||
style: "type-1"
|
||||
---
|
||||
|
||||
{/* Blockquotes */}
|
||||
import Ganbatte from "../../components/Post/Blockquotes/Ganbatte.astro";
|
||||
import Homework from "../../components/Post/Blockquotes/Homework.astro";
|
||||
import Important from "../../components/Post/Blockquotes/Important.astro";
|
||||
import Info from "../../components/Post/Blockquotes/Info.astro";
|
||||
import QA from "../../components/Post/Blockquotes/QA.astro";
|
||||
|
||||
import Spoiler from "../../components/Post/Spoiler.tsx";
|
||||
import QuantizationCalculator from "../../components/Util/QuantizationCalc.tsx";
|
||||
|
||||
# Hosting a Large Language Model (LLM) Locally
|
||||
|
||||
<picture>
|
||||
<img src="https://pic.mangopig.tech/i/879aaccd-6822-423f-883a-74cf5ba598e7.jpg" alt="Web Development Illustration" />
|
||||
</picture>
|
||||
|
||||
<blockquote class="lesson-meta">
|
||||
<span>Lesson 01</span>
|
||||
<span>Created at: **December 2025**</span>
|
||||
<span>Last Updated: **December 2025**</span>
|
||||
</blockquote>
|
||||
|
||||
<Ganbatte toc="Lesson Objectives" tocLevel="1" imageAlt="MangoPig Ganbatte">
|
||||
## Lesson Objectives
|
||||
|
||||
- Setting up your Developer Environment
|
||||
- Setting up a isolated Docker environment for hosting LLMs
|
||||
- Fetching the AI model
|
||||
- Converting the model to GGUF format
|
||||
- Quantizing the model for better performance
|
||||
- Hosting a basic LLM model with llama.cpp locally
|
||||
|
||||
</Ganbatte>
|
||||
|
||||
<section data-toc="Setting Up Developer Environment" data-toc-level="1">
|
||||
<h2>Setting Up Your Developer Environment</h2>
|
||||
<section data-toc="WSL" data-toc-level="2">
|
||||
<h3>Setting Up WSL (Windows Subsystem for Linux)</h3>
|
||||
To set up WSL on your Windows machine, follow these steps:
|
||||
1. Open PowerShell as Administrator.
|
||||
2. Run the following command to enable WSL and install a Linux distribution (Ubuntu is recommended):
|
||||
|
||||
```zsh frame="none"
|
||||
wsl --install
|
||||
```
|
||||
|
||||
3. Restart your computer when prompted.
|
||||
4. After restarting, open the Ubuntu application from the Start menu and complete the initial setup by creating a user account.
|
||||
5. Update your package lists and upgrade installed packages by running:
|
||||
|
||||
```zsh frame="none"
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
```
|
||||
</section>
|
||||
|
||||
<section data-toc="ZSH" data-toc-level="2">
|
||||
<h3>Getting Your Environment Ready</h3>
|
||||
|
||||
```zsh frame="none"
|
||||
sudo apt install -y git make curl sudo zsh
|
||||
```
|
||||
|
||||
```zsh frame="none"
|
||||
mkdir -p ~/Config/Dotfiles
|
||||
git clone https://git.mangopig.tech/MangoPig/Dot-Zsh.git ~/Config/Dotfiles/Zsh
|
||||
cd ~/Config/Dotfiles/Zsh
|
||||
```
|
||||
|
||||
Whenever there's a prompt to ask to install just confirm with `y` and hit enter.
|
||||
|
||||
```zsh frame="none"
|
||||
make setup
|
||||
```
|
||||
|
||||
Restart the shell to finalize the zsh setup:
|
||||
|
||||
```zsh frame="none"
|
||||
zsh
|
||||
```
|
||||
|
||||
With the above commands, you should have a zsh environment, coding language and Docker setup. We will get more in details of all the tools with this setup as we work through the lessons.
|
||||
</section>
|
||||
|
||||
<section data-toc="Docker" data-toc-level="2">
|
||||
<h3>Installing Docker</h3>
|
||||
Docker should already be installed with the above steps. To verify, run:
|
||||
|
||||
```zsh frame="none"
|
||||
docker --version
|
||||
```
|
||||
and try to run a test container:
|
||||
|
||||
```zsh frame="none"
|
||||
docker run hello-world
|
||||
```
|
||||
|
||||
If you run into permissions issues, you may need to add your user to the docker group:
|
||||
|
||||
```zsh frame="none"
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
|
||||
Then restart the shell or log out and back in by doing:
|
||||
|
||||
```zsh frame="none"
|
||||
zsh
|
||||
```
|
||||
|
||||
</section>
|
||||
|
||||
</section>
|
||||
|
||||
<section data-toc="Docker Environment Setup" data-toc-level="1">
|
||||
<h2>Setting Up the Isolated Docker Environment for Hosting LLMs</h2>
|
||||
Now that we have the local environment ready, we want to set up an isolated Docker environment for hosting LLMs so that it doesn't interfere with our main system.
|
||||
|
||||
<section data-toc="What is Docker?" data-toc-level="2">
|
||||
<h3>What is Docker?</h3>
|
||||
Docker is a platform that allows you to package your application and its dependencies into containers.
|
||||
|
||||
<Info>
|
||||
<span>You can find more Docker Images on <a href="https://hub.docker.com/">Docker Hub</a>.</span>
|
||||
</Info>
|
||||
|
||||
<section data-doc="Installing Docker" data-doc-level="3">
|
||||
<h4>Installing Docker</h4>
|
||||
|
||||
</section>
|
||||
|
||||
</section>
|
||||
|
||||
<section data-toc="Creating Docker Container" data-toc-level="2">
|
||||
<h3>Creating the Docker Container</h3>
|
||||
|
||||
For our current purpose, we will be using the official <a href="https://hub.docker.com/r/nvidia/cuda/tags">NVIDIA Docker image</a> so that we can leverage CUDA for GPU acceleration if available.
|
||||
|
||||
We will create the Docker container and make it interactive by running:
|
||||
|
||||
```zsh frame="none"
|
||||
docker run --gpus all -it --name llm-container -p 8080:8080 nvidia/cuda:13.0.2-cudnn-devel-ubuntu24.04 /bin/bash
|
||||
```
|
||||
|
||||
<Info>
|
||||
- `--gpus` all enables GPU support for the container.
|
||||
- `--it` makes the container interactive, allowing you to run commands inside it.
|
||||
- `--name` llm-container gives the container a name for easier reference.
|
||||
- `-p 8080:8080` = `-p HOST:CONTAINER` maps port 8080 on your host machine to port 8080 inside the container. This is useful if you plan to run a server inside the container and want to access it from your host machine.
|
||||
- `nvidia/cuda:13.0.2-cudnn-runtime-ubuntu24.04` specifies the Docker image to use.
|
||||
- `/bin/bash` start point for the container, which opens a bash shell.
|
||||
</Info>
|
||||
|
||||
Once you are inside the container, you can proceed to setup the environment like we did before in the <a href="#setting-up-developer-environment">WSL section</a>.
|
||||
|
||||
<Info>
|
||||
There's a few things you need to do before you can setup the Environment like we did before:
|
||||
1. Update the package lists and install necessary packages:
|
||||
```zsh frame="none"
|
||||
apt update && apt install -y git make curl sudo zsh
|
||||
```
|
||||
|
||||
2. Remove the default user (usually `ubuntu`) to avoid permission issues:
|
||||
```zsh frame="none"
|
||||
userdel -r ubuntu
|
||||
```
|
||||
|
||||
3. Run my provisional script to setup users and permissions:
|
||||
```zsh frame="none"
|
||||
bash <(curl -s https://git.mangopig.tech/mangopig/Dot-Zsh/raw/branch/main/scripts/provision.sh)
|
||||
```
|
||||
You should create your own user when prompted, make it have 1000 as UID and GID for consistency and please remember the password you set here as you'll need it to use `sudo` later on.
|
||||
|
||||
4. Now change users by doing: **(replace `your-username` with the username you created)**
|
||||
```zsh frame="none"
|
||||
su - your-username
|
||||
```
|
||||
|
||||
OR you can exit the container and reattach with the new user by doing:
|
||||
```zsh frame="none"
|
||||
exit
|
||||
docker start llm-container
|
||||
docker exec -it --user your-username llm-container /bin/zsh
|
||||
```
|
||||
Press `q` when they prompt you to create a zsh configuration file.
|
||||
|
||||
5. Now you can proceed to setup zsh and the rest of the environment as shown in the [previous section](#zsh).
|
||||
|
||||
</Info>
|
||||
|
||||
Try to do this on your own first! If you get stuck, you can check the solution below.
|
||||
|
||||
<Spoiler client:idle >
|
||||
## Solution
|
||||
|
||||
1. Update the package lists and install necessary packages:
|
||||
```zsh frame="none"
|
||||
apt update && apt install -y git make curl sudo zsh
|
||||
```
|
||||
|
||||
2. Remove the default user (usually `ubuntu`) to avoid permission issues:
|
||||
```zsh frame="none"
|
||||
userdel -r ubuntu
|
||||
```
|
||||
|
||||
3. Run my provisional script to setup users and permissions:
|
||||
```zsh frame="none"
|
||||
bash <(curl -s https://git.mangopig.tech/mangopig/Dot-Zsh/raw/branch/main/scripts/provision.sh)
|
||||
```
|
||||
You should create your own user when prompted, make it have 1000 as UID and GID for consistency and please remember the password you set here as you'll need it to use `sudo` later on.
|
||||
|
||||
4. Now change users by doing: **(replace `your-username` with the username you created)**
|
||||
```zsh frame="none"
|
||||
su - your-username
|
||||
```
|
||||
|
||||
OR you can exit the container and reattach with the new user by doing:
|
||||
```zsh frame="none"
|
||||
exit
|
||||
docker start llm-container
|
||||
docker exec -it --user your-username llm-container /bin/zsh
|
||||
```
|
||||
Press `q` when they prompt you to create a zsh configuration file.
|
||||
|
||||
5. Go into the dotfiles directory and setup zsh:
|
||||
```zsh frame="none"
|
||||
cd ~/Config/Dot-Zsh
|
||||
make base && \
|
||||
make python && \
|
||||
make clean && \
|
||||
make stow
|
||||
```
|
||||
|
||||
6. Restart the shell to finalize the zsh setup:
|
||||
```zsh frame="none"
|
||||
zsh
|
||||
```
|
||||
|
||||
7. Verify that Pyenv and Miniforge is working by:
|
||||
```zsh frame="none"
|
||||
pyenv --version
|
||||
conda --version
|
||||
```
|
||||
</Spoiler>
|
||||
</section>
|
||||
|
||||
</section>
|
||||
|
||||
<section data-toc="Python Setup" data-toc-level="1">
|
||||
<h2>Setting Up Python Environment</h2>
|
||||
Now that we have the Docker container set up, we can proceed to set up the environment to run llama.cpp inside the container.
|
||||
|
||||
We have setup `pyenv` and `Miniforge` as part of the zsh setup. You can verify that they are working by running:
|
||||
|
||||
```zsh frame="none"
|
||||
pyenv --version
|
||||
conda --version
|
||||
```
|
||||
|
||||
`pyenv` allows us to manage multiple Python versions easily. We can easily install different versions of Python and Conda environments as needed for different projects.
|
||||
|
||||
`conda` (via Miniforge) allows us to create isolated Python environments, which is helpful for making sure that the dependencies for llama.cpp do not interfere with other projects.
|
||||
|
||||
Let's first create a directory for llama.cpp and navigate into it:
|
||||
|
||||
```zsh frame="none"
|
||||
mkdir -p ~/Projects/llama.cpp
|
||||
cd ~/Projects/llama.cpp
|
||||
```
|
||||
|
||||
Now, let's clone the llama.cpp repository:
|
||||
|
||||
```zsh frame="none"
|
||||
git clone https://github.com/ggerganov/llama.cpp.git .
|
||||
```
|
||||
|
||||
<Info>
|
||||
- You can also the contents of the repository with `ls -la`
|
||||
- The `.` at the end of the git clone command ensures that the contents of the repository are cloned directly into the current directory.
|
||||
- For convenience, you can find the official llama.cpp repository at <a href="https://github.com/ggml-org/llama.cpp?tab=readme-ov-file">llama.cpp GitHub</a>
|
||||
</Info>
|
||||
|
||||
With the repository cloned, we can now proceed to build the llama.cpp.
|
||||
|
||||
We first use `cmake` to configure the build system. It's like telling the app what our computer environment looks like and what options we want to enable.
|
||||
|
||||
```zsh frame="none"
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=ON -DLLAMA_BUILD_SERVER=ON
|
||||
```
|
||||
|
||||
<Info>
|
||||
- `-S .` tells cmake where to find the source files (in this case, the current directory).
|
||||
- `-B build` specifies where all the temperary build files will go (in a folder named `build`).
|
||||
- `-G Ninja` tells cmake to use the Ninja build system.
|
||||
- `-DCMAKE_BUILD_TYPE=Release` sets the build type to Release for optimized performance.
|
||||
- `-DCMAKE_INSTALL_PREFIX=/your/install/dir` specifies where to install the built files. You can change this to your desired installation path.
|
||||
- `-DLLAMA_BUILD_TESTS=OFF` disables building tests.
|
||||
- `-DLLAMA_BUILD_EXAMPLES=ON` enables building example programs.
|
||||
- `-DLLAMA_BUILD_SERVER=ON` enables building the server component.
|
||||
</Info>
|
||||
|
||||
Now we can build the project, this step is basically taking what we told cmake to do and actually making it into executable files.
|
||||
|
||||
```zsh frame="none"
|
||||
cmake --build build --config Release -j $(nproc)
|
||||
```
|
||||
|
||||
<Info>
|
||||
- `--build build` tells cmake to build the project using the files in the `build` directory. (where we set with -B in the previous step)
|
||||
- `--config Release` specifies that we want to build the Release version.
|
||||
- `-j $(nproc)` tells cmake to use all available CPU cores for faster building.
|
||||
- `$(nproc)` is a command that returns the number of processing units available.
|
||||
</Info>
|
||||
|
||||
After we are doing building, the binaries will be located in the `build/bin` directory. We want to move it to a more accessible location (`/usr/local` that we specified earlier), so we can run it easily. We can do this by running:
|
||||
|
||||
```zsh frame="none"
|
||||
sudo cmake --install build && \
|
||||
sudo ldconfig
|
||||
```
|
||||
|
||||
<Info>
|
||||
- `--install build` tells cmake to install the built files from the `build` directory to the location we specified earlier with `-DCMAKE_INSTALL_PREFIX`.
|
||||
- `sudo ldconfig` updates the system's library cache to recognize the newly installed binaries.
|
||||
</Info>
|
||||
|
||||
Now you should be able to run the `llama.cpp` binary from anywhere, you can check what llama.cpp options are available by running:
|
||||
|
||||
```zsh frame="none"
|
||||
ls /usr/local/bin
|
||||
```
|
||||
|
||||
```zsh frame="none"
|
||||
bat llama-eval-callback llama-lookup llama-save-load-state
|
||||
convert_hf_to_gguf.py llama-export-lora llama-lookup-create llama-server
|
||||
fd llama-finetune llama-lookup-merge llama-simple
|
||||
llama-batched llama-gen-docs llama-lookup-stats llama-simple-chat
|
||||
llama-batched-bench llama-gguf llama-mtmd-cli llama-speculative
|
||||
llama-bench llama-gguf-hash llama-parallel llama-speculative-simple
|
||||
llama-cli llama-gguf-split llama-passkey llama-tokenize
|
||||
llama-convert-llama2c-to-ggml llama-idle llama-perplexity llama-tts
|
||||
llama-cvector-generator llama-imatrix llama-quantize
|
||||
llama-diffusion-cli llama-logits llama-retrieval
|
||||
llama-embedding llama-lookahead llama-run
|
||||
```
|
||||
|
||||
We can further verify whether we can run `llama.cpp` by checking its version:
|
||||
|
||||
```zsh frame="none"
|
||||
llama-cli --version
|
||||
```
|
||||
|
||||
```zsh frame="none"
|
||||
version: 7327 (c8554b66e)
|
||||
built with GNU 13.3.0 for Linux x86_64
|
||||
```
|
||||
|
||||
</section>
|
||||
|
||||
<section data-toc="Getting the AI" data-toc-level="1">
|
||||
<h2>Fetching the AI Model Weights</h2>
|
||||
Now that we have llama.cpp set up, we need to get some AI models to run with it.
|
||||
The main place to get models is from [Hugging Face](https://huggingface.co/). You will need to create an account if you don't have one already.
|
||||
Once you have created an account, you should also setup your access token by going:
|
||||
|
||||
<picture>
|
||||
<img src="https://pic.mangopig.tech/i/aea54c8e-9dd5-44c7-ab1f-6b57b076e7d8.webp" alt="Hugging Face Access Token" />
|
||||
</picture>
|
||||
|
||||
And then give your token all the `read` permissions.
|
||||
|
||||
<picture>
|
||||
<img src="https://pic.mangopig.tech/i/4360ee94-7f37-4897-91e9-882fd198b8b3.webp" alt="Hugging Face Token Permissions" />
|
||||
</picture>
|
||||
|
||||
<Important>
|
||||
Make sure to copy the token somewhere safe and **DO NOT SHARE IT WITH ANYONE** or **USE IT DIRECTLY IN PUBLIC REPOSITORIES** and **DIRECTLY IN YOUR CODE**! Consult AIs on how to keep your tokens safe if you are unsure, but do not directly share them with the AI.
|
||||
</Important>
|
||||
|
||||
Now that you have your token, you can use it to download models from Hugging Face. We will use `huggingface-cli` to do this. Let's first make the directory to store the models:
|
||||
|
||||
```zsh frame="none"
|
||||
mkdir -p ~/Models
|
||||
cd ~/Models
|
||||
```
|
||||
|
||||
We can then install `huggingface-cli`
|
||||
|
||||
```zsh frame="none"
|
||||
curl -LsSf https://hf.co/cli/install.sh | bash
|
||||
```
|
||||
|
||||
We will then login to Hugging Face using the CLI and provide our access token when prompted:
|
||||
|
||||
```zsh frame="none"
|
||||
git config --global credential.helper store
|
||||
```
|
||||
|
||||
```zsh frame="none"
|
||||
hf auth login
|
||||
```
|
||||
|
||||
```zsh frame="none"
|
||||
_| _| _| _| _|_|_| _|_|_| _|_|_| _| _| _|_|_| _|_|_|_| _|_| _|_|_| _|_|_|_|
|
||||
_| _| _| _| _| _| _| _|_| _| _| _| _| _| _| _|
|
||||
_|_|_|_| _| _| _| _|_| _| _|_| _| _| _| _| _| _|_| _|_|_| _|_|_|_| _| _|_|_|
|
||||
_| _| _| _| _| _| _| _| _| _| _|_| _| _| _| _| _| _| _|
|
||||
_| _| _|_| _|_|_| _|_|_| _|_|_| _| _| _|_|_| _| _| _| _|_|_| _|_|_|_|
|
||||
|
||||
To log in, `huggingface_hub` requires a token generated from https://huggingface.co/settings/tokens .
|
||||
Enter your token (input will not be visible): INPUT_YOUR_TOKEN_HERE
|
||||
Add token as git credential? [y/N]: y
|
||||
Token is valid (permission: fineGrained).
|
||||
The token `temp` has been saved to /home/mangopig/.cache/huggingface/stored_tokens
|
||||
Your token has been saved in your configured git credential helpers (store).
|
||||
Your token has been saved to /home/mangopig/.cache/huggingface/token
|
||||
Login successful.
|
||||
The current active token is: `temp`
|
||||
```
|
||||
|
||||
Now you can download models using the `hf download` command. I will be using the [`SmolLM3-3B`](https://huggingface.co/HuggingFaceTB/SmolLM3-3B) following this tutorial but if the model is too large for your system, you can choose a smaller model from Hugging Face, such as [`SmolLM2-1.7B`](https://huggingface.co/HuggingFaceTB/SmolLM2-1.7B) or [`SmolLM2-360M`](https://huggingface.co/HuggingFaceTB/SmolLM2-360M).
|
||||
|
||||
```zsh frame="none"
|
||||
hf download HuggingFaceTB/SmolLM3-3B --local-dir ~/Models/SmolLM3-3B
|
||||
```
|
||||
|
||||
<Info>
|
||||
- `HuggingFaceTB/SmolLM3-3B` is the model identifier on Hugging Face. Get it from clicking the button to copy the name in the image below:
|
||||
<picture>
|
||||
<img src="https://pic.mangopig.tech/i/674714b4-736b-429c-b198-c9d57ba8bdee.webp" alt="Hugging Face Model Page" />
|
||||
</picture>
|
||||
- `--local-dir ~/Models/SmolLM3-3B` specifies where to save the downloaded model.
|
||||
|
||||
You can find out more about what options you can use with `hf download` by doing `hf download --help`.
|
||||
|
||||
```zsh frame="none"
|
||||
> hf download --help
|
||||
|
||||
Usage: hf download [OPTIONS] REPO_ID [FILENAMES]...
|
||||
|
||||
Download files from the Hub.
|
||||
|
||||
Arguments:
|
||||
REPO_ID The ID of the repo (e.g. `username/repo-name`). [required]
|
||||
[FILENAMES]... Files to download (e.g. `config.json`,
|
||||
`data/metadata.jsonl`).
|
||||
|
||||
Options:
|
||||
--repo-type [model|dataset|space]
|
||||
The type of repository (model, dataset, or
|
||||
space). [default: model]
|
||||
--revision TEXT Git revision id which can be a branch name,
|
||||
a tag, or a commit hash.
|
||||
--include TEXT Glob patterns to include from files to
|
||||
download. eg: *.json
|
||||
--exclude TEXT Glob patterns to exclude from files to
|
||||
download.
|
||||
--cache-dir TEXT Directory where to save files.
|
||||
--local-dir TEXT If set, the downloaded file will be placed
|
||||
under this directory. Check out https://hugg
|
||||
ingface.co/docs/huggingface_hub/guides/downl
|
||||
oad#download-files-to-local-folder for more
|
||||
details.
|
||||
--force-download / --no-force-download
|
||||
If True, the files will be downloaded even
|
||||
if they are already cached. [default: no-
|
||||
force-download]
|
||||
--dry-run / --no-dry-run If True, perform a dry run without actually
|
||||
downloading the file. [default: no-dry-run]
|
||||
--token TEXT A User Access Token generated from
|
||||
https://huggingface.co/settings/tokens.
|
||||
--quiet / --no-quiet If True, progress bars are disabled and only
|
||||
the path to the download files is printed.
|
||||
[default: no-quiet]
|
||||
--max-workers INTEGER Maximum number of workers to use for
|
||||
downloading files. Default is 8. [default:
|
||||
8]
|
||||
--help Show this message and exit.
|
||||
```
|
||||
</Info>
|
||||
|
||||
With this, we have a model downloaded at `~/Models/SmolLM3-3B`. We can now proceed to try to run the model with llama.cpp.
|
||||
|
||||
</section>
|
||||
|
||||
<section data-toc="Converting Model to GGUF" data-toc-level="1">
|
||||
<h2>Converting the Model to GGUF</h2>
|
||||
<p>After downloading the model from Hugging Face, we need to convert it to the GGUF format so that llama.cpp can use it.</p>
|
||||
<p>Hugging Face usually store their models in the `.safetensors` format</p>
|
||||
<p>However, `llama.cpp` usually expect the models to be in the `.gguf` format.</p>
|
||||
<p>So we will need to convert the models to `.gguf`. Luckily, `llama.cpp` comes with a python script that helps us to do just that.</p>
|
||||
<p>We will first create a `Python` environment with `Conda` and activate it</p>
|
||||
|
||||
```zsh frame="none"
|
||||
conda create -n llama-cpp python=3.10 -y
|
||||
conda activate llama-cpp
|
||||
python -m pip install --upgrade pip wheel setuptools
|
||||
```
|
||||
|
||||
<Info>
|
||||
- `conda create -n llama-cpp python=3.10 -y` creates a new conda environment named `llama-cpp` with Python 3.10 installed
|
||||
- `-n`: Specifies the name of the environment.
|
||||
- `python=3.10`: Specifies the Python version to install in the environment.
|
||||
- `-y`: Automatically confirms the creation.
|
||||
- `conda activate llama-cpp` activates the newly created conda environment.
|
||||
- `python -m pip install --upgrade pip wheel setuptools`
|
||||
- We are updating `pip`, `wheel`, and `setuptools`
|
||||
- `pip`: The package installer for Python. Similar to `npm` and `go get` in other languages.
|
||||
- `wheel`: A built-package format for Python.
|
||||
- `setuptools`: A package development and distribution library for Python.
|
||||
</Info>
|
||||
|
||||
<p>`conda` is used to isolate the dependencies needed for the conversion process so that it doesn't interfere with other projects.</p>
|
||||
<p>We will then install the dependencies for `llama.cpp`</p>
|
||||
|
||||
```zsh frame="none"
|
||||
pip install --upgrade -r ~/Projects/llama.cpp/requirements/requirements-convert_hf_to_gguf.txt
|
||||
```
|
||||
|
||||
<Info>
|
||||
- `pip install`: Installs Python packages.
|
||||
- `--upgrade`: Upgrades the packages to the latest versions.
|
||||
- `-r`: Specifies that we are installing packages from a requirements file.
|
||||
- `~/Projects/llama.cpp/requirements/requirements-convert_hf_to_gguf.txt`: The path to the requirements file that contains the list of packages needed for converting models to GGUF format.
|
||||
</Info>
|
||||
|
||||
Nice! Now we are ready to convert the model to GGUF format. We can do this by running the conversion script provided by `llama.cpp`
|
||||
|
||||
```zsh frame="none"
|
||||
python ~/Projects/llama.cpp/convert_hf_to_gguf.py \
|
||||
~/Models/SmolLM3-3B \
|
||||
--outfile ~/Models/SmolLM3-3B/SmolLM3-3B.gguf
|
||||
```
|
||||
|
||||
<Info>
|
||||
- `python ~/Projects/llama.cpp/convert_hf_to_gguf.py`: `python` runs the conversion script located at `~/Projects/llama.cpp/scripts/convert_hf_to_gguf.py`.
|
||||
- `~/Models/SmolLM3-3B`: Specifies the path to the downloaded model in Hugging Face format.
|
||||
- `--outfile ~/Models/SmolLM3-3B/SmolLM3-3B.gguf`: Specifies where to save the converted model in GGUF format.
|
||||
</Info>
|
||||
|
||||
When you see a similar output to:
|
||||
|
||||
```zsh frame="none"
|
||||
INFO:hf-to-gguf:Model successfully exported to SmolLM3-3B.gguf
|
||||
```
|
||||
|
||||
Then you have succeeded in converting the model to GGUF format!
|
||||
|
||||
</section>
|
||||
|
||||
<section data-toc="Quantizing the Model" data-toc-level="1">
|
||||
<h2>Quantizing the Model for Better Performance</h2>
|
||||
<p>Quantization is a technique used to reduce the size of the model and improve inference speed and VRAM requirements by compressing and reducing the model's weight</p>
|
||||
|
||||
We can learn what quantization `llama.cpp` supports by running:
|
||||
|
||||
```zsh frame="none"
|
||||
llama-quantize --help
|
||||
```
|
||||
|
||||
```zsh frame="none"
|
||||
usage: llama-quantize [--help] [--allow-requantize] [--leave-output-tensor] [--pure] [--imatrix] [--include-weights]
|
||||
[--exclude-weights] [--output-tensor-type] [--token-embedding-type] [--tensor-type] [--prune-layers] [--keep-split] [--override-kv]
|
||||
model-f32.gguf [model-quant.gguf] type [nthreads]
|
||||
|
||||
--allow-requantize: Allows requantizing tensors that have already been quantized. Warning: This can severely reduce quality compared to quantizing from 16bit or 32bit
|
||||
--leave-output-tensor: Will leave output.weight un(re)quantized. Increases model size but may also increase quality, especially when requantizing
|
||||
--pure: Disable k-quant mixtures and quantize all tensors to the same type
|
||||
--imatrix file_name: use data in file_name as importance matrix for quant optimizations
|
||||
--include-weights tensor_name: use importance matrix for this/these tensor(s)
|
||||
--exclude-weights tensor_name: use importance matrix for this/these tensor(s)
|
||||
--output-tensor-type ggml_type: use this ggml_type for the output.weight tensor
|
||||
--token-embedding-type ggml_type: use this ggml_type for the token embeddings tensor
|
||||
--tensor-type TENSOR=TYPE: quantize this tensor to this ggml_type. example: --tensor-type attn_q=q8_0
|
||||
Advanced option to selectively quantize tensors. May be specified multiple times.
|
||||
--prune-layers L0,L1,L2...comma-separated list of layer numbers to prune from the model
|
||||
Advanced option to remove all tensors from the given layers
|
||||
--keep-split: will generate quantized model in the same shards as input
|
||||
--override-kv KEY=TYPE:VALUE
|
||||
Advanced option to override model metadata by key in the quantized model. May be specified multiple times.
|
||||
Note: --include-weights and --exclude-weights cannot be used together
|
||||
|
||||
Allowed quantization types:
|
||||
2 or Q4_0 : 4.34G, +0.4685 ppl @ Llama-3-8B
|
||||
3 or Q4_1 : 4.78G, +0.4511 ppl @ Llama-3-8B
|
||||
38 or MXFP4_MOE : MXFP4 MoE
|
||||
8 or Q5_0 : 5.21G, +0.1316 ppl @ Llama-3-8B
|
||||
9 or Q5_1 : 5.65G, +0.1062 ppl @ Llama-3-8B
|
||||
19 or IQ2_XXS : 2.06 bpw quantization
|
||||
20 or IQ2_XS : 2.31 bpw quantization
|
||||
28 or IQ2_S : 2.5 bpw quantization
|
||||
29 or IQ2_M : 2.7 bpw quantization
|
||||
24 or IQ1_S : 1.56 bpw quantization
|
||||
31 or IQ1_M : 1.75 bpw quantization
|
||||
36 or TQ1_0 : 1.69 bpw ternarization
|
||||
37 or TQ2_0 : 2.06 bpw ternarization
|
||||
10 or Q2_K : 2.96G, +3.5199 ppl @ Llama-3-8B
|
||||
21 or Q2_K_S : 2.96G, +3.1836 ppl @ Llama-3-8B
|
||||
23 or IQ3_XXS : 3.06 bpw quantization
|
||||
26 or IQ3_S : 3.44 bpw quantization
|
||||
27 or IQ3_M : 3.66 bpw quantization mix
|
||||
12 or Q3_K : alias for Q3_K_M
|
||||
22 or IQ3_XS : 3.3 bpw quantization
|
||||
11 or Q3_K_S : 3.41G, +1.6321 ppl @ Llama-3-8B
|
||||
12 or Q3_K_M : 3.74G, +0.6569 ppl @ Llama-3-8B
|
||||
13 or Q3_K_L : 4.03G, +0.5562 ppl @ Llama-3-8B
|
||||
25 or IQ4_NL : 4.50 bpw non-linear quantization
|
||||
30 or IQ4_XS : 4.25 bpw non-linear quantization
|
||||
15 or Q4_K : alias for Q4_K_M
|
||||
14 or Q4_K_S : 4.37G, +0.2689 ppl @ Llama-3-8B
|
||||
15 or Q4_K_M : 4.58G, +0.1754 ppl @ Llama-3-8B
|
||||
17 or Q5_K : alias for Q5_K_M
|
||||
16 or Q5_K_S : 5.21G, +0.1049 ppl @ Llama-3-8B
|
||||
17 or Q5_K_M : 5.33G, +0.0569 ppl @ Llama-3-8B
|
||||
18 or Q6_K : 6.14G, +0.0217 ppl @ Llama-3-8B
|
||||
7 or Q8_0 : 7.96G, +0.0026 ppl @ Llama-3-8B
|
||||
1 or F16 : 14.00G, +0.0020 ppl @ Mistral-7B
|
||||
32 or BF16 : 14.00G, -0.0050 ppl @ Mistral-7B
|
||||
0 or F32 : 26.00G @ 7B
|
||||
COPY : only copy tensors, no quantizing
|
||||
```
|
||||
|
||||
<Info>
|
||||
For a line `2 or Q4_0 : 4.34G, +0.4685 ppl @ Llama-3-8B`
|
||||
- `2` and `Q4_0` are the identifiers you can use to specify the quantization type.
|
||||
- `4.34G` indicates the size of the quantized model.
|
||||
- `+0.4685 ppl` indicates the increase in perplexity (a measure of model performance; lower is better) when using this quantization type
|
||||
</Info>
|
||||
|
||||
<QA>
|
||||
<span slot="question">How do I know how big of a model size can I fit in my computer</span>
|
||||
<p>It depends on whether you are running inference on your <strong>CPU (System RAM)</strong> or <strong>GPU (VRAM)</strong>.</p>
|
||||
|
||||
<p>For CPU inference, you generally want the model size to be around 2x the size of your system RAM for comfortable operation. For example, if you have 16GB of RAM, you should aim for models that are around 8GB or smaller.</p>
|
||||
|
||||
**Size (GB) ≈ (Parameters (Billions) × Bits Per Weight) / 8 + Overhead**
|
||||
|
||||
- Bits Per Weight (bpw):
|
||||
- Qx = x bits per weight
|
||||
- Qx_K = K quants will keep some important weights at higher precision (Q4_K ≈ 5 bits per weight, Q5_K ≈ 6 bits per weight, Q6_K ≈ 7 bits per weight)
|
||||
- Qx_K_S = Small K quants
|
||||
- Qx_K_M = Medium K quants
|
||||
- Qx_K_L = Large K quants
|
||||
- IQx = Integer Quantization with x bits per weight, bpw is on the chart
|
||||
- TQx = Ternary Quantization with x bits per weight, bpw is on the chart
|
||||
</QA>
|
||||
|
||||
<QuantizationCalculator client:idle />
|
||||
|
||||
Once we have decided what quantization type to use, we can proceed to quantize the model by running:
|
||||
|
||||
```zsh frame="none"
|
||||
llama-quantize \
|
||||
~/Models/SmolLM3-3B/SmolLM3-3B.gguf \
|
||||
~/Models/SmolLM3-3B/SmolLM3-3B.q4.gguf \
|
||||
q4_0
|
||||
4
|
||||
```
|
||||
|
||||
<Info>
|
||||
- `llama-quantize`: The command to run the quantization process.
|
||||
- `~/Models/SmolLM3-3B/SmolLM3-3B.gguf`: The path to the original GGUF model that we want to quantize.
|
||||
- `~/Models/SmolLM3-3B/SmolLM3-3B.q4.gguf`: The path where we want to save the quantized model.
|
||||
- `q4_0`: The quantization type we want to use (in this case, Q4_0).
|
||||
- `4`: Number of threads to use for quantization (optional, defaults to number of CPU cores).
|
||||
</Info>
|
||||
|
||||
<p>After the quantization is complete, you should see a new file named `SmolLM3-3B.q4.gguf` in the model directory.</p>
|
||||
<p>We can now learn how to serve the model with `llama.cpp`</p>
|
||||
|
||||
</section>
|
||||
|
||||
<section data-toc="Inferencing the Model" data-toc-level="1">
|
||||
<h2>Inferencing the Model</h2>
|
||||
<p>Now that we have the model ready, we can proceed to run inference with it using `llama.cpp`.</p>
|
||||
<p>`llama.cpp` provides us with multiple ways of inferencing, we can: </p>
|
||||
- Use the command line interface (CLI) to interact with the model directly from the terminal. (llama-cli)
|
||||
- Use the server mode to host the model and interact with it via HTTP requests. (llama-server)
|
||||
|
||||
For this tutorial, we will use the `llama-server` to serve the model.
|
||||
|
||||
To start the server with our quantized model, we can run:
|
||||
|
||||
```zsh frame="none"
|
||||
llama-server \
|
||||
--model ~/Models/SmolLM3-3B/SmolLM3-3B.q4.gguf \
|
||||
--host 0.0.0.0 \
|
||||
--port 8080
|
||||
```
|
||||
|
||||
<Info>
|
||||
- `llama-server`: The command to start the server.
|
||||
- `--model ~/Models/SmolLM3-3B/SmolLM3-3B.q4.gguf`: Specifies the path to the quantized model we want to serve.
|
||||
- `--host 0.0.0.0`: This makes the server accessible from any IP address.
|
||||
- `--port 8080`: Specifies the port on which the server will listen for incoming requests.
|
||||
You can read all the options you can customize to run the server [here](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md)
|
||||
</Info>
|
||||
|
||||
As soon as you see this
|
||||
|
||||
```zsh frame="none"
|
||||
main: model loaded
|
||||
main: server is listening on http://0.0.0.0:8080
|
||||
main: starting the main loop...
|
||||
```
|
||||
|
||||
Your server is up and running! You can now interact with the model by going to [`http://localhost:8080`](http://localhost:8080) in your web browser or using tools like `curl` for API requests.
|
||||
|
||||
Open another terminal window and use this example for API request using `curl`:
|
||||
|
||||
```zsh frame="none"
|
||||
curl \
|
||||
--request POST \
|
||||
--url http://localhost:8080/completion \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{"prompt": "Building a website can be done in 10 simple steps:","n_predict": 128}'
|
||||
```
|
||||
|
||||
<Info>
|
||||
- `--request POST`: Specifies that we are making a POST request. (We will get into REST HTTP APIs in future tutorials)
|
||||
- `--url http://localhost:8080/completion`: The URL of the server endpoint for completions.
|
||||
- `--header "Content-Type: application/json"`: Sets the content type to JSON.
|
||||
- `--data '{...}'`: The JSON payload containing the prompt and other parameters for the model.
|
||||
|
||||
Read more about the API requests [here](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md#using-with-curl)
|
||||
</Info>
|
||||
|
||||
</section>
|
||||
|
||||
<section data-toc="Docker Volume Mount" data-toc-level="1">
|
||||
<h2>Docker Volume Mount</h2>
|
||||
|
||||
Before we continue, we are going to destroy everything that we have worked on so far:
|
||||
|
||||
```zsh frame="none"
|
||||
exit # As many times as needed to exit the container to your host shell
|
||||
docker stop llm-container
|
||||
docker rm llm-container
|
||||
```
|
||||
|
||||
This is to show that, whenever we remove the Docker container, all the data inside the container will be lost. This is bad because we don't want to redownload and reconvert the models every time we restart the container.
|
||||
|
||||
To solve this issue, we can use Docker volume mounts to persist our data.
|
||||
|
||||
Docker volume maps directories from your host machine to the Docker container.
|
||||
It's a little bit like plugging in a USB drive to your computer, so that the data on the USB drive is accessible even if you remove the USB drive.
|
||||
|
||||
When you run the Docker container, you can use the `-v` option to specify volume mounts.
|
||||
|
||||
```zsh frame="none"
|
||||
docker run \
|
||||
--gpus all \
|
||||
-it \
|
||||
-v ~/Models:/Models \
|
||||
--name llm-container \
|
||||
-p 8080:8080 \
|
||||
nvidia/cuda:13.0.2-cudnn-devel-ubuntu24.04 \
|
||||
/bin/bash
|
||||
```
|
||||
|
||||
<Info>
|
||||
- `-v ~/Models:/Models`: This maps the `~/Models` directory on your host machine to the `/Models` directory inside the Docker container.
|
||||
- The left side (`~/Models`) is the path on your host machine.
|
||||
- The right side (`/Models`) is the path inside the Docker container.
|
||||
- With this setup, any models you download to `~/Models` on your host machine will be accessible at `/Models` inside the Docker container, and vice versa.
|
||||
</Info>
|
||||
|
||||
Now, it's your turn to set up everything again inside the Docker container, but this time, when you download and convert the models, make sure to save them to the `/Models` directory inside the container. Try to do it own your own!
|
||||
|
||||
<Homework>
|
||||
<h3>Your Task</h3>
|
||||
1. Setting up Hugging Face CLI and downloading the model to `~/Models` in your host machine
|
||||
2. Starting a docker container and mount `~/Models` to `/Models` in the container
|
||||
3. Initializing the container with the scripts provided
|
||||
- apt update and install dependencies
|
||||
- delete default user
|
||||
- provisional script
|
||||
- log into to your own user account
|
||||
4. Cloning llama.cpp and building it
|
||||
5. Converting the model to GGUF and quantizing it (Remember your models are in `/Models` now!)
|
||||
6. Running the server with the model from `/Models`
|
||||
</Homework>
|
||||
|
||||
The solution is below if you get stuck:
|
||||
|
||||
<Spoiler client:idle>
|
||||
|
||||
1. Setting up Hugging Face CLI and downloading the model to `~/Models` in your host machine
|
||||
|
||||
```zsh frame="none"
|
||||
mkdir -p ~/Models
|
||||
cd ~/Models
|
||||
curl -LsSf https://hf.co/cli/install.sh | bash
|
||||
git config --global credential.helper store
|
||||
hf auth login
|
||||
hf download HuggingFaceTB/SmolLM3-3B --local-dir ~/Models/SmolLM3-3B
|
||||
```
|
||||
|
||||
2. Starting a docker container and mount `~/Models` to `/Models` in the container
|
||||
|
||||
```zsh frame="none"
|
||||
docker run \
|
||||
--gpus all \
|
||||
-it \
|
||||
-v ~/Models:/Models \
|
||||
--name llm-container \
|
||||
-p 8080:8080 \
|
||||
nvidia/cuda:13.0.2-cudnn-devel-ubuntu24.04 \
|
||||
/bin/bash
|
||||
```
|
||||
|
||||
3. Initializing the container with the scripts provided
|
||||
|
||||
```zsh frame="none"
|
||||
apt update && apt install -y git make curl sudo zsh
|
||||
userdel -r ubuntu
|
||||
bash <(curl -s https://git.mangopig.tech/mangopig/Dot-Zsh/raw/branch/main/scripts/provision.sh)
|
||||
su - mangopig
|
||||
```
|
||||
|
||||
```zsh frame="none"
|
||||
cd ~/Config/Dot-Zsh
|
||||
make base && \
|
||||
make python && \
|
||||
make clean && \
|
||||
make stow && \
|
||||
zsh
|
||||
```
|
||||
|
||||
OR you can just run:
|
||||
|
||||
```zsh frame="none"
|
||||
cd ~/Config/Dot-Zsh
|
||||
make setup && \
|
||||
zsh
|
||||
```
|
||||
4. Cloning llama.cpp and building it
|
||||
|
||||
```zsh frame="none"
|
||||
mkdir -p ~/Projects/llama.cpp
|
||||
cd ~/Projects/llama.cpp
|
||||
git clone https://github.com/ggerganov/llama.cpp.git .
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=ON -DLLAMA_BUILD_SERVER=ON
|
||||
cmake --build build --config Release -j $(nproc)
|
||||
sudo cmake --install build && \
|
||||
sudo ldconfig
|
||||
```
|
||||
|
||||
5. Converting the model to GGUF and quantizing it (Remember your models are in `/Models` now!)
|
||||
|
||||
```zsh frame="none"
|
||||
conda create -n llama-cpp python=3.10 -y
|
||||
conda activate llama-cpp
|
||||
python -m pip install --upgrade pip wheel setuptools
|
||||
pip install --upgrade -r ~/Projects/llama.cpp/requirements/requirements-convert_hf_to_gguf.txt
|
||||
python ~/Projects/llama.cpp/convert_hf_to_gguf.py \
|
||||
/Models/SmolLM3-3B \
|
||||
--outfile /Models/SmolLM3-3B/SmolLM3-3B.gguf
|
||||
llama-quantize \
|
||||
/Models/SmolLM3-3B/SmolLM3-3B.gguf \
|
||||
/Models/SmolLM3-3B/SmolLM3-3B.q4.gguf \
|
||||
q4_0
|
||||
4
|
||||
```
|
||||
|
||||
6. Running the server with the model from `/Models`
|
||||
|
||||
```zsh frame="none"
|
||||
llama-server \
|
||||
--model /Models/SmolLM3-3B/SmolLM3-3B.q4.gguf \
|
||||
--host 0.0.0.0
|
||||
--port 8080
|
||||
```
|
||||
|
||||
</Spoiler>
|
||||
|
||||
If you have done it without help! Congratulations! You have successfully set up a persistent environment for running llama.cpp with Docker volume mounts!
|
||||
|
||||
<h3 data-toc="Conclusion" data-toc-level="1">Wrapping Up</h3>
|
||||
|
||||
Your LLM setup will still stop when you stop the container tho. In the future, we will learn more about that will help solve these issues:
|
||||
|
||||
- Creating Custom Docker Images to Preserve Setup
|
||||
- Deploying LLM Server to the Cloud
|
||||
- Hosting Multiple Models and Switching Between Them
|
||||
- Using docker-compose to Manage Multiple Containers
|
||||
|
||||
<h3 data-toc="Tmux Session Persistence" data-toc-level="2">Tmux Session Persistence</h3>
|
||||
For now, if you want to keep the server running after exiting the terminal, you can use `tmux` or `screen` to create a persistent session inside the Docker container.
|
||||
|
||||
1. Enter the Docker container again (if you have exited it):
|
||||
|
||||
```zsh frame="none"
|
||||
docker start llm-container
|
||||
```
|
||||
|
||||
```zsh frame="none"
|
||||
docker exec -it --user YOUR_USERNAME llm-container /bin/zsh
|
||||
```
|
||||
|
||||
2. Install `tmux` inside the container
|
||||
|
||||
```zsh frame="none"
|
||||
sudo apt install -y tmux
|
||||
tmux new -s llm-server
|
||||
```
|
||||
|
||||
3. Start the server inside the `tmux` session
|
||||
|
||||
```zsh frame="none"
|
||||
llama-server \
|
||||
--model /Models/SmolLM3-3B/SmolLM3-3B.q4.gguf \
|
||||
--host 0.0.0.0
|
||||
--port 8080
|
||||
```
|
||||
|
||||
4. To detach from the `tmux` session and keep it running in the background, press `Ctrl + B`, then `D`.
|
||||
|
||||
5. To reattach to the `tmux` session later, use:
|
||||
```zsh frame="none"
|
||||
tmux attach -t llm-server
|
||||
```
|
||||
|
||||
<h3 data-toc="Basic Container Management" data-toc-level="2">Basic Container Management</h3>
|
||||
|
||||
This session will persist as long as the Docker container is running. Your setup will also persist as long as you don't remove the Docker container. But if you want to free up some resources, you should stop the container when not in use.
|
||||
|
||||
You can stop the docker container with:
|
||||
|
||||
```zsh frame="none"
|
||||
docker stop llm-container
|
||||
```
|
||||
|
||||
You can remove the container with:
|
||||
|
||||
```zsh frame="none"
|
||||
docker rm llm-container
|
||||
```
|
||||
|
||||
Start it back up anytime with:
|
||||
|
||||
```zsh frame="none"
|
||||
docker start llm-container
|
||||
```
|
||||
|
||||
Reattach to the container with:
|
||||
|
||||
```zsh frame="none"
|
||||
docker exec -it --user YOUR_USERNAME llm-container /bin/zsh
|
||||
```
|
||||
|
||||
</section>
|
||||
@ -1,15 +0,0 @@
|
||||
// Path: 00-Lesson-Site/src/content/lessons/config.ts
|
||||
|
||||
import { defineCollection, z } from "astro:content";
|
||||
|
||||
const lessonsCollection = defineCollection({
|
||||
type: "content",
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
lessons: lessonsCollection,
|
||||
};
|
||||
@ -1,42 +0,0 @@
|
||||
---
|
||||
// Path: src/helpers/colorMode.astro
|
||||
---
|
||||
|
||||
<script is:inline>
|
||||
// 1. Basic function to set theme on load
|
||||
function getTheme() {
|
||||
const stored = localStorage.getItem("theme");
|
||||
if (stored) return stored;
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
if (theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
}
|
||||
}
|
||||
|
||||
// Run on initial load
|
||||
setTheme(getTheme());
|
||||
|
||||
// 2. THE FIX: Intercept the new page BEFORE the swap
|
||||
document.addEventListener("astro:before-swap", (event) => {
|
||||
// Read the current class from the OLD document (the one you are looking at)
|
||||
const isDark = document.documentElement.classList.contains("dark");
|
||||
|
||||
// Apply it to the NEW document (the one about to slide in)
|
||||
// This ensures the "snapshot" of the new page is already dark.
|
||||
if (isDark) {
|
||||
event.newDocument.documentElement.classList.add("dark");
|
||||
event.newDocument.documentElement.setAttribute("data-theme", "dark");
|
||||
} else {
|
||||
// Explicitly remove it for light mode to be safe
|
||||
event.newDocument.documentElement.classList.remove("dark");
|
||||
event.newDocument.documentElement.setAttribute("data-theme", "light");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -1,36 +0,0 @@
|
||||
---
|
||||
// Path: 00-Lesson-Site/frontend/src/layouts/LandingLayout.astro
|
||||
|
||||
import { ClientRouter } from "astro:transitions";
|
||||
|
||||
import Navbar from "../components/Navbar/Navbar.astro";
|
||||
import ThemeScript from "../helpers/colorMode.astro";
|
||||
import "../styles/main.scss";
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<ClientRouter />
|
||||
|
||||
<!-- Color Mode Helper -->
|
||||
<ThemeScript />
|
||||
|
||||
<!-- Favicon Icons -->
|
||||
<link rel="icon" href="favicon/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-icon.png" />
|
||||
<link rel="manifest" href="favicon/site.webmanifest" />
|
||||
|
||||
<title>Web Dev Lesson Notes</title>
|
||||
</head>
|
||||
<body>
|
||||
<Navbar />
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,46 +0,0 @@
|
||||
---
|
||||
// Path: 00-Lesson-Site/frontend/src/layouts/LessonLayout.astro
|
||||
|
||||
import { ClientRouter } from "astro:transitions";
|
||||
|
||||
import Navbar from "../components/Navbar/Navbar.astro";
|
||||
import FloatingTOC from "../components/Post/FloatingTOC.astro";
|
||||
import ThemeScript from "../helpers/colorMode.astro";
|
||||
import "../styles/main.scss";
|
||||
|
||||
interface Props {
|
||||
pageTitle?: string;
|
||||
}
|
||||
|
||||
const { pageTitle = "Web Dev Lessons" } = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<ClientRouter />
|
||||
|
||||
<!-- Color Mode Helper -->
|
||||
<ThemeScript />
|
||||
|
||||
<!-- Favicon Icons -->
|
||||
<link rel="icon" href="favicon/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-icon.png" />
|
||||
<link rel="manifest" href="favicon/site.webmanifest" />
|
||||
|
||||
<title>{pageTitle}</title>
|
||||
</head>
|
||||
<body>
|
||||
<Navbar />
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<FloatingTOC />
|
||||
</body>
|
||||
</html>
|
||||
@ -1,13 +0,0 @@
|
||||
---
|
||||
// Path: 00-Lesson-Site/frontend/src/pages/changelog.astro
|
||||
|
||||
import Layout from "../layouts/LessonLayout.astro";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<h1>THIS PAGE IS EMPTY</h1>
|
||||
<p>But you can find the lessons here!</p>
|
||||
<ul>
|
||||
<li><a href="lessons/01-intro">Lesson 01!</a></li>
|
||||
</ul>
|
||||
</Layout>
|
||||
@ -1,13 +0,0 @@
|
||||
---
|
||||
// Path: 00-Lesson-Site/frontend/src/pages/index.astro
|
||||
|
||||
import Layout from "../layouts/LessonLayout.astro";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<h1>THIS PAGE IS EMPTY</h1>
|
||||
<p>But you can find the lessons here!</p>
|
||||
<ul>
|
||||
<li><a href="lessons/01-intro">Lesson 01!</a></li>
|
||||
</ul>
|
||||
</Layout>
|
||||
@ -1,35 +0,0 @@
|
||||
---
|
||||
// Path: src/pages/lessons/[slug].astro
|
||||
|
||||
import { getCollection, type CollectionEntry } from "astro:content";
|
||||
import LessonLayout from "../../layouts/LessonLayout.astro";
|
||||
|
||||
import styles from "./lessonPage.module.scss";
|
||||
|
||||
interface Props {
|
||||
entry: CollectionEntry<"lessons">;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const lessonEntries = await getCollection("lessons");
|
||||
|
||||
return lessonEntries.map((entry: CollectionEntry<"lessons">) => ({
|
||||
params: { slug: entry.slug },
|
||||
props: { entry },
|
||||
}));
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await entry.render();
|
||||
|
||||
// Dynamically Import Lesson Style from the entry's frontmatter
|
||||
if (entry.data.style) {
|
||||
await import(`../../styles/lessons/${entry.data.style}.scss`);
|
||||
}
|
||||
---
|
||||
|
||||
<LessonLayout pageTitle={entry.data.title}>
|
||||
<div class:list={[styles.content]} id="lesson-container">
|
||||
<Content />
|
||||
</div>
|
||||
</LessonLayout>
|
||||
@ -1,13 +0,0 @@
|
||||
---
|
||||
// Path: 00-Lesson-Site/frontend/src/pages/lessons/index.astro
|
||||
|
||||
import Layout from "../../layouts/LessonLayout.astro";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<h1>THIS PAGE IS EMPTY</h1>
|
||||
<p>But you can find the lessons here!</p>
|
||||
<ul>
|
||||
<li><a href="lessons/01-intro">Lesson 01!</a></li>
|
||||
</ul>
|
||||
</Layout>
|
||||
@ -1,12 +0,0 @@
|
||||
/* Path: frontend/src/pages/lessons/lessonPage.module.scss */
|
||||
|
||||
.content {
|
||||
width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
margin-bottom: 200px;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
---
|
||||
// Path: 00-Lesson-Site/frontend/src/pages/resources.astro
|
||||
|
||||
import Layout from "../layouts/LessonLayout.astro";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<h1>THIS PAGE IS EMPTY</h1>
|
||||
<p>But you can find the lessons here!</p>
|
||||
<ul>
|
||||
<li><a href="lessons/01-intro">Lesson 01!</a></li>
|
||||
</ul>
|
||||
</Layout>
|
||||
@ -1,13 +0,0 @@
|
||||
/* Path: src/styles/_fonts.scss */
|
||||
|
||||
@font-face {
|
||||
font-family: "Geist";
|
||||
src: url("/fonts/Geist.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "GeistMono";
|
||||
src: url("/fonts/GeistMono.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
/* Path: src/styles/_global_vars.scss */
|
||||
|
||||
@use "sass:meta";
|
||||
|
||||
@function color-adjust($color, $contrast-boost: 0, $c-offset: 0) {
|
||||
$L: calc(var(--#{meta.inspect($color)}-base-l) - (#{$contrast-boost} * var(--theme-polarity)));
|
||||
$C: calc(var(--#{meta.inspect($color)}-base-c) + #{$c-offset});
|
||||
$H: var(--#{meta.inspect($color)}-base-h);
|
||||
|
||||
@return oklch(#{$L} #{$C} #{$H});
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
/* Path: src/styles/_reset.scss */
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
img,
|
||||
picture,
|
||||
video,
|
||||
canvas,
|
||||
svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
#root,
|
||||
#__next {
|
||||
isolation: isolate;
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
/* Path: src/styles/lessons/type-1.scss */
|
||||
|
||||
.lesson-meta {
|
||||
font-size: 0.9em;
|
||||
color: #888;
|
||||
background-color: color-adjust(background, 0.01, 0.01);
|
||||
margin-bottom: 20px;
|
||||
border-radius: 20px;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5em;
|
||||
color: #ff5e5e;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
color: #ffbd72;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.75em;
|
||||
color: #aad4fc;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.5em;
|
||||
color: #9dffb8;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ffb8b8;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: #ff5e5e;
|
||||
}
|
||||
}
|
||||
|
||||
picture {
|
||||
display: block;
|
||||
margin: 20px auto;
|
||||
max-width: 100%;
|
||||
|
||||
img {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(252, 241, 145, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
p,
|
||||
li,
|
||||
blockquote {
|
||||
line-height: 1.6;
|
||||
margin-bottom: 15px;
|
||||
text-align: justify;
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
color: #ffb8b8;
|
||||
}
|
||||
|
||||
.expressive-code {
|
||||
margin: 20px !important;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: color-adjust(background, 0.02, 0.02);
|
||||
color: color-adjust(primary, 0, 0);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-family: "GeistMono", monospace;
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
/* Path: src/styles/main.scss */
|
||||
|
||||
:root {
|
||||
--theme-polarity: 1;
|
||||
|
||||
--background-base-l: 0.99;
|
||||
--background-base-c: 0.02;
|
||||
--background-base-h: 100;
|
||||
|
||||
--text-base-l: 0.4;
|
||||
--text-base-c: 0.015;
|
||||
--text-base-h: 84;
|
||||
|
||||
--primary-base-l: 0.75;
|
||||
--primary-base-c: 0.145;
|
||||
--primary-base-h: 142;
|
||||
|
||||
--secondary-base-l: 0.75;
|
||||
--secondary-base-c: 0.06;
|
||||
--secondary-base-h: 60;
|
||||
}
|
||||
|
||||
@mixin dark-values {
|
||||
--theme-polarity: -1;
|
||||
|
||||
--background-base-l: 0.1;
|
||||
--background-base-c: 0.015;
|
||||
--background-base-h: 84;
|
||||
|
||||
--text-base-l: 0.95;
|
||||
--text-base-c: 0.015;
|
||||
--text-base-h: 84;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
@include dark-values;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
@include dark-values;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: color-adjust(background, 0, 0);
|
||||
color: color-adjust(text, 0, 0);
|
||||
|
||||
transition:
|
||||
background-color 0.4s ease,
|
||||
color 0.4s ease;
|
||||
|
||||
font-family: "Geist", sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
nav,
|
||||
button,
|
||||
hr,
|
||||
input,
|
||||
textarea,
|
||||
.icon,
|
||||
svg {
|
||||
transition:
|
||||
background-color 0.4s ease,
|
||||
color 0.4s ease,
|
||||
border-color 0.4s ease,
|
||||
fill 0.4s ease,
|
||||
stroke 0.4s ease;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flow-root;
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [
|
||||
".astro/types.d.ts",
|
||||
"**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"dist"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js"
|
||||
}
|
||||
}
|
||||