adding the first version of the presentation and the initial attempt at the .meetings script

This commit is contained in:
itsamejms
2026-06-07 16:13:10 +01:00
parent 41f99557cc
commit 05c7d6aef3
34 changed files with 7623 additions and 1 deletions
+4
View File
@@ -0,0 +1,4 @@
*.mp3
*.bin
.DS_Store
*.wav
+4
View File
@@ -0,0 +1,4 @@
STT_ENGINE=whisper
OLLAMA_MODEL=llama3.1:8b
THREADS=4
STT_MODEL=/Users/jamestwose/Coding/ledex-demo/.meetings/models/ggml-small.en.bin
+7
View File
@@ -1 +1,8 @@
## The local AI demo for LEDEX Events
Tools used:
- [pi](https://pi.dev)
## Demos
- Sentiment analysis of reviews + learning points
- Meeting recording + summarization + action points
+8
View File
@@ -0,0 +1,8 @@
node_modules/
build/
dist/
coverage/
.DS_Store
*.log
.env*
!.env.example
+1
View File
@@ -0,0 +1 @@
## Local AI presentation for LEDEX Portimao 2026
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Local AI Presentation</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
{
"name": "Local AI Presentation",
"description": "A responsive Sovereign/Cyber presentation carousel on Local AI for Business.",
"requestFramePermissions": []
}
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
{
"name": "local-ai-presentation",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port=5173 --host=0.0.0.0",
"build": "vite build",
"preview": "vite preview",
"clean": "rm -rf dist server.js",
"lint": "tsc --noEmit"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
"dotenv": "^17.2.3",
"express": "^4.21.2",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"vite": "^6.2.3"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.14.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"autoprefixer": "^10.4.21",
"esbuild": "^0.25.0",
"tailwindcss": "^4.1.14",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.3"
}
}
+248
View File
@@ -0,0 +1,248 @@
import { useState, useEffect, useRef } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { synth } from './utils/audioSynth';
import { SLIDES } from './data/slides';
import { GoalType, BudgetType } from './types';
// Slide components
import Slide1Title from './components/slides/Slide1Title';
import Slide2CloudParadox from './components/slides/Slide2CloudParadox';
import Slide3LocalAIStack from './components/slides/Slide3LocalAIStack';
import Slide4Quantization from './components/slides/Slide4Quantization';
import Slide5Pipelines from './components/slides/Slide5Pipelines';
import Slide6HardwareMatrix from './components/slides/Slide6HardwareMatrix';
import Slide7BuyersGuide from './components/slides/Slide7BuyersGuide';
import Slide8ROI from './components/slides/Slide8ROI';
import Slide9OfflineDemo from './components/slides/Slide9OfflineDemo';
// Layout components
import ControlBar from './components/layout/ControlBar';
export default function App() {
const [currentSlide, setCurrentSlide] = useState<number>(0);
const [direction, setDirection] = useState<number>(1);
const [isAutoplay, setIsAutoplay] = useState<boolean>(false);
const [soundEnabled, setSoundEnabled] = useState<boolean>(false);
// Shared state for cross-slide communication
const [selectedGoal, setSelectedGoal] = useState<GoalType>('agents');
const [procureBudget, setProcureBudget] = useState<BudgetType>('budget');
const [toastMessage, setToastMessage] = useState<string | null>(null);
const autoplayTimerRef = useRef<NodeJS.Timeout | null>(null);
const showToast = (msg: string) => {
setToastMessage(msg);
synth.playBlip();
setTimeout(() => {
setToastMessage(null);
}, 4000);
};
// Sound configuration binding
useEffect(() => {
synth.enabled = soundEnabled;
}, [soundEnabled]);
// Keyboard navigation
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'ArrowRight') {
nextSlide();
} else if (e.key === 'ArrowLeft') {
prevSlide();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [currentSlide]);
// Autoplay effect
useEffect(() => {
if (isAutoplay) {
autoplayTimerRef.current = setInterval(() => {
setDirection(1);
setCurrentSlide((prev) => (prev + 1) % SLIDES.length);
synth.playSlideChange();
}, 7000);
} else {
if (autoplayTimerRef.current) {
clearInterval(autoplayTimerRef.current);
}
}
return () => {
if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
};
}, [isAutoplay]);
const nextSlide = () => {
setDirection(1);
setCurrentSlide((prev) => (prev + 1) % SLIDES.length);
synth.playSlideChange();
};
const prevSlide = () => {
setDirection(-1);
setCurrentSlide((prev) => (prev - 1 + SLIDES.length) % SLIDES.length);
synth.playSlideChange();
};
const jumpToSlide = (index: number) => {
setDirection(index > currentSlide ? 1 : -1);
setCurrentSlide(index);
synth.playSlideChange();
};
// Frame motion sliding definition
const slideVariants = {
enter: (dir: number) => ({
x: dir > 0 ? 300 : -300,
opacity: 0,
scale: 0.98
}),
center: {
x: 0,
opacity: 1,
scale: 1,
transition: {
x: { type: 'spring' as const, stiffness: 220, damping: 24 },
opacity: { duration: 0.25 },
scale: { duration: 0.3 }
}
},
exit: (dir: number) => ({
x: dir < 0 ? 300 : -300,
opacity: 0,
scale: 0.98,
transition: {
x: { duration: 0.25 },
opacity: { duration: 0.2 }
}
})
};
// Render current slide content
const renderSlide = () => {
switch (currentSlide) {
case 0:
return <Slide1Title />;
case 1:
return <Slide2CloudParadox />;
case 2:
return <Slide3LocalAIStack />;
case 3:
return <Slide4Quantization />;
case 4:
return <Slide5Pipelines showToast={showToast} />;
case 5:
return <Slide6HardwareMatrix
selectedGoal={selectedGoal}
onGoalChange={setSelectedGoal}
procureBudget={procureBudget}
onBudgetChange={setProcureBudget}
/>;
case 6:
return <Slide7BuyersGuide
showToast={showToast}
selectedGoal={selectedGoal}
procureBudget={procureBudget}
onGoalChange={setSelectedGoal}
onBudgetChange={setProcureBudget}
/>;
case 7:
return <Slide8ROI
selectedGoal={selectedGoal}
procureBudget={procureBudget}
showToast={showToast}
/>;
case 8:
return <Slide9OfflineDemo showToast={showToast} />;
default:
return <Slide1Title />;
}
};
return (
<div id="sovereign-app" className="min-h-screen bg-slate-950 text-slate-100 selection:bg-emerald-500 selection:text-slate-950 flex flex-col justify-between p-4 sm:p-6 md:p-8 relative overflow-hidden cyber-grid font-sans">
{/* Dynamic scanline overlay */}
<div className="absolute inset-0 scanline pointer-events-none opacity-20 z-10" />
{/* Background radial gradient glow */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] rounded-full bg-emerald-500/5 blur-[120px] pointer-events-none" />
{/* CENTRAL CAROUSEL CONTAINER */}
<main className="w-full max-w-7xl mx-auto my-6 sm:my-8 flex-1 flex flex-col justify-center items-center z-20">
{/* Main Console Board Frame */}
<div className="w-full bg-slate-900/40 border border-slate-800/80 rounded-xl relative overflow-hidden flex flex-col backdrop-blur-md cyber-glow shadow-2xl">
{/* High-tech edge detail bracket and scan bar */}
<div className="absolute top-0 inset-x-0 h-[1px] bg-gradient-to-r from-transparent via-emerald-500/20 to-transparent" />
<div className="absolute left-0 inset-y-0 w-[1px] bg-gradient-to-b from-transparent via-emerald-500/10 to-transparent" />
{/* Console Top Info Segment */}
<div className="px-4 py-2 bg-slate-900/80 border-b border-slate-800/80 flex items-center justify-between font-mono text-[10px] text-slate-500">
<div className="flex items-center gap-4">
{/* <span className="text-emerald-500/70 font-bold">▶ INDEX_READOUT // SEC_0{currentSlide + 1}</span>
<span className="hidden sm:inline">|</span>
<span className="hidden sm:inline">GRID_COORDINATES: CL-517 // EN-891</span> */}
</div>
<div className="flex items-center gap-3">
<span className="text-[9px] px-2 py-0.5 rounded bg-slate-950 text-slate-400 border border-slate-800/80">{SLIDES[currentSlide].category}</span>
<span className="font-bold tracking-widest">0{currentSlide + 1} // 0{SLIDES.length}</span>
</div>
</div>
{/* PRIMARY ANIMATED CAROUSEL VIEWPORTS */}
<div className="relative min-h-[480px] sm:min-h-[520px] md:min-h-[580px] p-6 sm:p-8 md:p-12 overflow-hidden flex flex-col justify-center">
<AnimatePresence mode="wait" custom={direction}>
<motion.div
key={currentSlide}
custom={direction}
variants={slideVariants}
initial="enter"
animate="center"
exit="exit"
className="w-full h-full flex flex-col justify-between"
>
{renderSlide()}
</motion.div>
</AnimatePresence>
</div>
{/* LOWER CONSOLE CONTROL BAR */}
<ControlBar
currentSlide={currentSlide}
isAutoplay={isAutoplay}
onPrevSlide={prevSlide}
onNextSlide={nextSlide}
onJumpToSlide={jumpToSlide}
onToggleAutoplay={() => setIsAutoplay(!isAutoplay)}
/>
</div>
</main>
{/* Toast Notification */}
{toastMessage && (
<div className="fixed bottom-8 right-8 bg-emerald-500 text-slate-950 px-4 py-3 rounded-lg font-mono text-xs font-bold shadow-[0_0_20px_rgba(16,185,129,0.4)] z-50 animate-in fade-in slide-in-from-bottom-4">
{toastMessage}
</div>
)}
{/* Sound toggle button (optional feature) */}
{/* <button
onClick={() => setSoundEnabled(!soundEnabled)}
className="fixed top-4 right-4 p-2 rounded-lg bg-slate-900/80 border border-slate-800 text-slate-400 hover:text-emerald-400 transition-colors z-50"
title={soundEnabled ? 'Disable sound effects' : 'Enable sound effects'}
>
{soundEnabled ? '🔊' : '🔇'}
</button> */}
</div>
);
}
@@ -0,0 +1,94 @@
import { ChevronLeft, ChevronRight, Play, Pause } from 'lucide-react';
import { SLIDES } from '../../data/slides';
import { synth } from '../../utils/audioSynth';
interface ControlBarProps {
currentSlide: number;
isAutoplay: boolean;
onPrevSlide: () => void;
onNextSlide: () => void;
onJumpToSlide: (index: number) => void;
onToggleAutoplay: () => void;
}
export default function ControlBar({
currentSlide,
isAutoplay,
onPrevSlide,
onNextSlide,
onJumpToSlide,
onToggleAutoplay
}: ControlBarProps) {
const handleElementClick = () => synth.playClick();
return (
<footer className="px-4 py-3 sm:py-4 bg-slate-900/80 border-t border-slate-800/80 flex flex-col sm:flex-row items-center justify-between gap-4 font-mono z-30">
<div className="flex items-center gap-1.5 text-slate-500 text-[10px] order-3 sm:order-1">
<span className="px-1.5 py-0.5 rounded bg-slate-950 border border-slate-800 text-[9px]"></span>
<span className="px-1.5 py-0.5 rounded bg-slate-950 border border-slate-800 text-[9px]"></span>
<span className="hidden sm:inline font-sans text-[10px]">keys to navigate</span>
</div>
<div className="flex items-center gap-2 order-1 sm:order-2">
{SLIDES.map((slide, idx) => (
<button
key={slide.id}
onClick={() => {
synth.playClick();
onJumpToSlide(idx);
}}
className={`relative w-2 h-2 rounded-full cursor-pointer transition-all duration-300 ${
currentSlide === idx
? 'bg-emerald-400 w-6 shadow-[0_0_8px_rgba(16,185,129,0.5)]'
: 'bg-slate-700 hover:bg-slate-500'
}`}
title={`Jump to slide ${slide.id}: ${slide.title}`}
/>
))}
</div>
<div className="flex items-center gap-2 order-2 sm:order-3 w-full sm:w-auto justify-between sm:justify-start">
<button
onClick={() => {
handleElementClick();
onToggleAutoplay();
}}
className={`px-3 py-1.5 rounded border text-[10px] flex items-center gap-1.5 cursor-pointer transition-all duration-150 ${
isAutoplay
? 'border-emerald-500/40 bg-emerald-950/20 text-emerald-400'
: 'border-slate-800 bg-slate-950 text-slate-400 hover:text-slate-300 hover:border-slate-700'
}`}
title="Autoplay transitions through slides"
>
{isAutoplay ? <Pause className="w-3 h-3 text-emerald-400 animate-pulse" /> : <Play className="w-3 h-3" />}
<span>{isAutoplay ? 'AUTOPLAY: ON' : 'AUTOPLAY: OFF'}</span>
</button>
<div className="flex items-center gap-2">
<button
onClick={() => {
synth.playClick();
onPrevSlide();
}}
className="px-3.5 py-1.5 rounded border border-slate-800 bg-slate-950 text-slate-300 text-[10px] font-bold hover:border-slate-700 hover:text-white hover:bg-slate-900 cursor-pointer transition-all flex items-center gap-1"
>
<ChevronLeft className="w-3.5 h-3.5" />
<span>BACK</span>
</button>
<button
onClick={() => {
synth.playClick();
onNextSlide();
}}
className="px-3.5 py-1.5 rounded border border-emerald-500 bg-emerald-500 text-slate-950 text-[10px] font-black tracking-wide hover:opacity-90 hover:shadow-[0_0_10px_rgba(16,185,129,0.3)] cursor-pointer transition-all flex items-center gap-1"
>
<span>{currentSlide === SLIDES.length - 1 ? 'REPLAY' : 'NEXT'}</span>
<ChevronRight className="w-3.5 h-3.5" />
</button>
</div>
</div>
</footer>
);
}
@@ -0,0 +1,42 @@
import { motion } from 'motion/react';
import { Cpu } from 'lucide-react';
export default function Slide1Title() {
return (
<div className="flex flex-col items-center justify-center text-center py-6 sm:py-10 max-w-3xl mx-auto">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.1, duration: 0.5 }}
className="relative mb-10"
>
<div className="w-24 h-24 sm:w-28 sm:h-28 rounded-full border border-emerald-500/15 border-dashed flex items-center justify-center animate-[spin_40s_linear_infinite]" />
<div className="absolute inset-2 rounded-full border border-emerald-500/30 flex items-center justify-center animate-[spin_20s_linear_infinite_reverse]" />
<div className="absolute inset-4 rounded-full border-2 border-emerald-500/60 border-t-transparent border-b-transparent flex items-center justify-center animate-[spin_8s_linear_infinite]" />
<div className="absolute inset-0 m-auto w-12 h-12 rounded-full bg-emerald-950/40 border border-emerald-500/50 flex items-center justify-center">
<Cpu className="w-5 h-5 text-emerald-400 animate-pulse" />
</div>
</motion.div>
<p className="text-emerald-500 font-mono text-xs sm:text-sm tracking-widest uppercase font-bold text-glow mb-4">
INTRODUCTION
</p>
<h1 className="font-display font-extrabold text-3xl sm:text-4xl md:text-5xl lg:text-5xl text-white tracking-tight leading-tight uppercase font-glow mb-5">
The Sovereign Business Engine
</h1>
<div className="h-[2px] w-24 bg-gradient-to-r from-transparent via-emerald-500 to-transparent mb-6" />
<p className="text-slate-400 font-sans text-base sm:text-lg md:text-xl font-light leading-relaxed max-w-xl">
Unlock Secure, Zero-Cost Local AI for Algarve Entrepreneurs, Nomads & Content Creators
</p>
<div className="mt-12 flex flex-wrap gap-3 font-mono text-[10px] text-slate-400 justify-center">
<span className="px-3 py-1.5 rounded-full bg-slate-950 border border-emerald-900/30"> PRIVATE INTEL</span>
<span className="px-3 py-1.5 rounded-full bg-slate-950 border border-emerald-900/30"> DECENTRALISED</span>
<span className="px-3 py-1.5 rounded-full bg-slate-950 border border-emerald-900/30"> 0% SAAS TRANSACTION</span>
</div>
</div>
);
}
@@ -0,0 +1,107 @@
import { useState } from 'react';
import { Info } from 'lucide-react';
import { synth } from '../../utils/audioSynth';
interface ParadoxCard {
title: string;
desc: string;
detail: string;
alternative: string;
color: string;
tag: string;
}
const paradoxCards: ParadoxCard[] = [
{
title: "Client Data Leaks",
desc: "Privacy Risk",
detail: "Inputting client names, email campaigns, or proprietary concepts online sends delicate parameters straight to external tech servers.",
alternative: "Complete Sandboxed Privacy. No outbound internet transfers, keeping secret client details locked safely inside your laptop.",
color: "from-rose-500/10 to-rose-600/5 border-rose-950/40 text-rose-400",
tag: "DATA RISK"
},
{
title: "Subscription Taxes",
desc: "Licensing Cost",
detail: "Paying $20$40 per person every single month creates heavy baseline overheads for growing digital agencies or self-starters.",
alternative: "Zero-Bill Scale. Run identical models on modern laptops with zero cumulative licensing bills or user caps.",
color: "from-amber-500/10 to-amber-600/5 border-amber-950/40 text-amber-400",
tag: "CASH BURN"
},
{
title: "Wi-Fi Dependencies",
desc: "Broadband Risks",
detail: "Spotty beach Wi-Fi, hotel dropouts, or cafe broadband failures freeze active cloud access, instantly stalling content work.",
alternative: "100% Offline Power. Work continuously from the beach, a rural guest country house, or a transit plane in ultimate peace.",
color: "from-orange-500/10 to-orange-600/5 border-orange-950/40 text-orange-400",
tag: "OFFLINE DOWNTIME"
}
];
export default function Slide2CloudParadox() {
const [selectedParadox, setSelectedParadox] = useState<number | null>(null);
const handleCardClick = (idx: number) => {
synth.playClick();
setSelectedParadox(selectedParadox === idx ? null : idx);
};
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center max-w-6xl mx-auto w-full">
<div className="lg:col-span-4 flex flex-col justify-center">
<span className="text-emerald-500 font-mono text-xs font-bold uppercase tracking-wider mb-2">// THE CONUNDRUM</span>
<h2 className="font-display font-bold text-2xl sm:text-3xl text-white tracking-tight uppercase leading-none mb-4">
The Cloud Expense Trap
</h2>
<p className="text-slate-400 font-sans text-sm sm:text-base leading-relaxed mb-6">
Relying on big-tech online platforms invites data leakage risks, builds high monthly software subscriptions, and depends on having perfect Internet. Tap each risk card to unlock your secure local remedy.
</p>
<div className="hidden lg:flex items-center gap-2 border border-slate-800/80 rounded px-3 py-1.5 bg-slate-900/50 text-[10px] font-mono text-slate-500">
<Info className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
<span>Interactive: Click each card to reveal the offline local alternative.</span>
</div>
</div>
<div className="lg:col-span-8 grid grid-cols-1 md:grid-cols-3 gap-4 w-full h-auto">
{paradoxCards.map((card, idx) => (
<div
key={idx}
id={`paradox-card-${idx}`}
onClick={() => handleCardClick(idx)}
className={`group relative p-5 rounded-lg border bg-gradient-to-b transition-all duration-300 cursor-pointer text-left flex flex-col justify-between min-h-[220px] md:min-h-[265px] ${
selectedParadox === idx
? 'border-emerald-500/65 bg-slate-950/80 shadow-[0_0_15px_rgba(16,185,129,0.15)]'
: `border-slate-800/80 bg-slate-900/60 hover:border-slate-700 hover:bg-slate-900/90`
}`}
>
<div>
<div className="flex justify-between items-start mb-4">
<span className="font-mono text-[9px] font-bold text-slate-500">PARADOX_0{idx + 1}</span>
<span className={`font-mono text-[8px] font-semibold px-2 py-0.5 rounded ${
selectedParadox === idx ? 'bg-emerald-950/40 text-emerald-400' : 'bg-slate-950/60 text-slate-400'
}`}>
{selectedParadox === idx ? 'SOVEREIGN_REMEDY' : card.tag}
</span>
</div>
<h3 className="font-display font-medium text-white text-base tracking-tight mb-2">
{selectedParadox === idx ? 'Antidote' : card.title}
</h3>
<p className="font-sans text-[12px] leading-relaxed text-slate-400">
{selectedParadox === idx ? card.alternative : card.detail}
</p>
</div>
<div className="pt-4 border-t border-slate-850 justify-self-end mt-4">
<span className="font-mono text-[9px] text-emerald-500 group-hover:underline flex items-center gap-1.5">
{selectedParadox === idx ? '← SHOW RISK METRIC' : '⚡ REVEAL LOCAL ANTIDOTE'}
</span>
</div>
</div>
))}
</div>
</div>
);
}
@@ -0,0 +1,104 @@
import { useState } from 'react';
import { Layers } from 'lucide-react';
import { synth } from '../../utils/audioSynth';
interface StackLayer {
level: number;
label: string;
desc: string;
color: string;
}
const stackLayers: StackLayer[] = [
{
level: 3,
label: "Visual Desktop Apps (The App)",
desc: "Simple visual programs like AnythingLLM or LM Studio designed for easy point-and-click work.",
color: "from-emerald-400/20 to-emerald-500/10 border-emerald-500/35 hover:scale-[1.01]"
},
{
level: 2,
label: "Open-Weights Models (The Brain)",
desc: "Highly optimized intelligence units (Llama, Mistral, Qwen) saved directly to your computer.",
color: "from-emerald-600/15 to-emerald-700/5 border-emerald-500/25 hover:scale-[1.01]"
},
{
level: 1,
label: "On-Device Processing (The Hardware)",
desc: "The fast processor chips or built-in system memory already in your office computer.",
color: "from-emerald-800/10 to-slate-900/40 border-slate-700/60 hover:scale-[1.01]"
}
];
const layerDetails: Record<number, string> = {
3: "FRIENDLY VISUAL DESKTOP APPS: Point-and-click programs that look and feel just like ChatGPT. Supports dragging files for secure offline searches, organizing text templates, and polishing emails. Popular: LM Studio, AnythingLLM, Jan.",
2: "INDEPENDENT FREE BRAINS: Highly capable modular model weights that you download once for free. Ranging in size depending on task complexity, they execute completely locally on your computer with zero monthly limits.",
1: "YOUR BUILT-IN SILICON: The physical components already inside your device. Modern Apple MacBook silicon or standard business graphics processors calculate AI answers in fractions of a second."
};
export default function Slide3LocalAIStack() {
const [activeStackLayer, setActiveStackLayer] = useState<number>(1);
const handleLayerClick = (level: number) => {
synth.playClick();
setActiveStackLayer(level);
};
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center max-w-6xl mx-auto w-full">
<div className="lg:col-span-5 flex flex-col justify-center">
<span className="text-emerald-500 font-mono text-xs font-bold uppercase tracking-wider mb-2">// COGNITIVE STRUCTURE</span>
<h2 className="font-display font-bold text-2xl sm:text-3xl text-white tracking-tight uppercase leading-none mb-4">
Your Private AI Workspace
</h2>
<p className="text-slate-400 font-sans text-sm sm:text-base leading-relaxed mb-6">
Running secure local AI is straightforward and divides into three simple, clear levels. Click any layer below to see how friendly desktop apps link directly to your laptop's power.
</p>
<div className="bg-slate-950/40 border border-emerald-950/30 rounded-lg p-5 font-mono text-left">
<div className="flex items-center gap-2 text-emerald-400 text-xs font-bold mb-2">
<Layers className="w-4 h-4" />
<span>LEVEL_0{activeStackLayer} SUMMARY</span>
</div>
<p className="text-[12px] leading-relaxed text-slate-300">
{layerDetails[activeStackLayer]}
</p>
</div>
</div>
<div className="lg:col-span-7 flex flex-col gap-3 items-center justify-center w-full relative">
{stackLayers.map((layer) => (
<div
key={layer.level}
id={`stack-layer-${layer.level}`}
onClick={() => handleLayerClick(layer.level)}
className={`w-full p-5 rounded-lg border text-left cursor-pointer transition-all duration-300 relative ${layer.color} ${
activeStackLayer === layer.level
? 'border-emerald-400 bg-slate-910/80 shadow-[0_0_20px_rgba(16,185,129,0.2)]'
: 'bg-slate-900/40 hover:bg-slate-900/80'
}`}
>
<div className="flex justify-between items-center mb-1">
<span className="font-mono text-[9px] tracking-widest text-emerald-400 font-bold">LEVEL_0{layer.level} SYSTEM LAYER</span>
{activeStackLayer === layer.level && (
<span className="font-mono text-[9px] text-emerald-400 text-glow flex items-center gap-1 animate-pulse">
● SELECTED_RUN
</span>
)}
</div>
<h3 className="font-display font-medium text-white text-base tracking-tight mb-1">
{layer.label}
</h3>
<p className="font-sans text-[12px] text-slate-400 leading-relaxed">
{layer.desc}
</p>
</div>
))}
<div className="absolute right-6 top-1/4 bottom-1/4 w-[1px] border-r border-dashed border-emerald-500/20 -z-10 pointer-events-none hidden md:block" />
</div>
</div>
);
}
@@ -0,0 +1,143 @@
import { useState } from 'react';
import { QuantLevel } from '../../types';
import { synth } from '../../utils/audioSynth';
interface QuantOption {
level: QuantLevel;
label: string;
ram: string;
intellect: string;
speed: string;
desc: string;
}
const quantOptions: QuantOption[] = [
{ level: 'FP16', label: 'FP16 Raw', ram: "16 GB", intellect: "100%", speed: "1.0x", desc: "No loss, heavy workload" },
{ level: 'Q8_0', label: '8-Bit (Q8)', ram: "8.5 GB", intellect: "99.1%", speed: "1.8x", desc: "Premium, lower RAM" },
{ level: 'Q4_K_M', label: '4-Bit (Q4)', ram: "4.8 GB", intellect: "96.2%", speed: "3.2x", desc: "Sovereign sweetspot" },
{ level: 'Q2_K', label: '2-Bit (Q2)', ram: "2.9 GB", intellect: "81.3%", speed: "4.5x", desc: "Highly crushed, rapid" }
];
const ramValues: Record<QuantLevel, string> = {
'FP16': '16.0 GB',
'Q8_0': '8.5 GB',
'Q4_K_M': '4.8 GB',
'Q2_K': '2.9 GB'
};
const ramPercentages: Record<QuantLevel, string> = {
'FP16': '100%',
'Q8_0': '53.1%',
'Q4_K_M': '30.0%',
'Q2_K': '18.1%'
};
const intellectValues: Record<QuantLevel, string> = {
'FP16': '100%',
'Q8_0': '99.1%',
'Q4_K_M': '96.2%',
'Q2_K': '81.3%'
};
const speedValues: Record<QuantLevel, { display: string; percent: string }> = {
'FP16': { display: '1.0x (Slow)', percent: '22%' },
'Q8_0': { display: '1.8x (Fast)', percent: '40%' },
'Q4_K_M': { display: '3.2x (Rapid)', percent: '71%' },
'Q2_K': { display: '4.5x (Ultrafast)', percent: '100%' }
};
const visualSampling: Record<QuantLevel, string> = {
'FP16': '[░░░██████████████████░░░] [Raw-grade maximum detail coordinates - extremely heavy] MAXIMUM_DETAIL',
'Q8_0': '[░░░█████████████████░░░░] [Highly accurate - lighter RAM footprint] SAFE_BUDGET',
'Q4_K_M': '[░░░░███████████████░░░░░] [Sovereign Sweet Spot - runs ultra fast on any Macbook or PC] SYSTEM_RECOMMENDED',
'Q2_K': '[░░░░░░░██████████░░░░░░░] [Crushed data structure - logic levels degraded] REDUCED_LOGIC'
};
export default function Slide4Quantization() {
const [quantLevel, setQuantLevel] = useState<QuantLevel>('Q4_K_M');
const handleQuantClick = (level: QuantLevel) => {
synth.playClick();
setQuantLevel(level);
};
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center max-w-6xl mx-auto w-full">
<div className="lg:col-span-5 flex flex-col justify-center">
<span className="text-emerald-500 font-mono text-xs font-bold uppercase tracking-wider mb-2">// DYNAMIC OPTIMISATION</span>
<h2 className="font-display font-bold text-2xl sm:text-3xl text-white tracking-tight uppercase leading-none mb-4">
AI Compression: Smart Scaling
</h2>
<p className="text-slate-400 font-sans text-sm leading-relaxed mb-6">
<strong>Analogy:</strong> 4K Cinema Raw vs. 1080p Stream. Quantization compresses the model parameters (e.g., 16-bit floats to 4-bit integers), cutting RAM requirements by up to 75% while retaining ~95%+ cognitive fidelity.
</p>
<div className="bg-slate-950/40 border border-slate-800/80 rounded-lg p-4 font-mono text-left">
<span className="text-[10px] text-slate-400 block mb-1">SYSTEM INSIGHT</span>
<p className="text-[11px] text-slate-400 leading-relaxed">
By compressing heavy decimal parameters to integer coordinates, we reduce the speed bottlenecks caused by bandwidth throughput limiters, yielding 3x faster local response feedback directly on simple laptops.
</p>
</div>
</div>
<div className="lg:col-span-7 flex flex-col gap-6 w-full">
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-5 sm:p-6 md:p-8 relative">
<div className="flex items-center justify-between font-mono text-[10px] text-slate-400 mb-6">
<span>INTERACTIVE COMPRESSION SELECTOR</span>
<span className="text-emerald-500 font-bold">MODE: {quantLevel}</span>
</div>
<div className="grid grid-cols-4 gap-2 mb-8">
{quantOptions.map((item) => (
<button
key={item.level}
onClick={() => handleQuantClick(item.level)}
className={`p-3 rounded-lg border text-center font-mono cursor-pointer transition-all duration-200 flex flex-col justify-between ${
quantLevel === item.level
? 'border-emerald-500 bg-emerald-950/25 text-emerald-400 shadow-[0_0_10px_rgba(16,185,129,0.15)]'
: 'border-slate-800 bg-slate-900/40 text-slate-400 hover:border-slate-700 hover:text-slate-200'
}`}
>
<span className="text-xs font-bold leading-none mb-1">{item.level}</span>
<span className="text-[9px] text-slate-500 font-light leading-none">{item.label}</span>
</button>
))}
</div>
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="p-3 bg-slate-900/80 rounded-lg border border-slate-850">
<span className="font-mono text-[9px] text-slate-500 block mb-1 uppercase">RAM footprints</span>
<span className="font-display font-medium text-lg text-white">{ramValues[quantLevel]}</span>
<div className="w-full bg-slate-950 h-1.5 rounded-full overflow-hidden mt-1 px-0.5 relative">
<div className="h-full bg-emerald-500 transition-all duration-300" style={{ width: ramPercentages[quantLevel] }} />
</div>
</div>
<div className="p-3 bg-slate-900/80 rounded-lg border border-slate-850">
<span className="font-mono text-[9px] text-slate-500 block mb-1 uppercase">Intelligence Match</span>
<span className="font-display font-medium text-lg text-white">{intellectValues[quantLevel]}</span>
<div className="w-full bg-slate-950 h-1.5 rounded-full overflow-hidden mt-1 px-0.5">
<div className="h-full bg-emerald-500 transition-all duration-300" style={{ width: intellectValues[quantLevel] }} />
</div>
</div>
<div className="p-3 bg-slate-900/80 rounded-lg border border-slate-850">
<span className="font-mono text-[9px] text-slate-500 block mb-1 uppercase">Output Speedup</span>
<span className="font-display font-medium text-lg text-emerald-400">{speedValues[quantLevel].display}</span>
<div className="w-full bg-slate-950 h-1.5 rounded-full overflow-hidden mt-1 px-0.5">
<div className="h-full bg-emerald-400 transition-all duration-300" style={{ width: speedValues[quantLevel].percent }} />
</div>
</div>
</div>
<div className="p-3 bg-slate-950 rounded border border-slate-850 font-mono text-left relative overflow-hidden">
<span className="text-[9px] text-slate-500 block mb-1">VISUAL VECTOR SAMPLING GRAPHICS:</span>
<div className="text-[10px] sm:text-xs text-slate-300 tracking-normal break-all select-none">
{visualSampling[quantLevel]}
</div>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,432 @@
import { useState } from 'react';
import { RefreshCw, Copy, Brain, Tags, TrendingUp, Mail } from 'lucide-react';
import { synth } from '../../utils/audioSynth';
type PipelineType = 'triage' | 'classifier' | 'leads';
interface PipelineOption {
id: PipelineType;
label: string;
icon: React.ReactNode;
description: string;
}
const pipelineOptions: PipelineOption[] = [
{
id: 'triage',
label: '📧 EMAIL TRIAGE',
icon: <Mail className="w-4 h-4" />,
description: 'Auto-categorize & prioritize inbox'
},
{
id: 'classifier',
label: '🏷️ CONTENT TAGGER',
icon: <Tags className="w-4 h-4" />,
description: 'Classify content by type & tone'
},
{
id: 'leads',
label: '📊 LEAD SCORER',
icon: <TrendingUp className="w-4 h-4" />,
description: 'Score & rank sales prospects'
}
];
const sampleInputs: Record<PipelineType, string> = {
triage: `From: Maria Santos <maria@techstartup.pt>
Subject: URGENT - Partnership proposal for AI automation
Hi team, we're looking to implement local AI across our 50-person marketing agency. Need quote by Friday. Budget: €15k.
---
From: João Pereira <joao@algarvevillas.com>
Subject: Re: Villa booking inquiry #4821
Thank you for the quick response! Can you send availability for July 15-22? 6 guests.
---
From: billing@cloudservice.io
Subject: Invoice #8821 - Payment Overdue
Your account is 30 days past due. Immediate payment required to avoid suspension.`,
classifier: `POST 1: "Just closed our biggest deal using local AI! No more monthly fees, zero data leaks. Algarve businesses, this is the future! 🚀 #LocalAI #BusinessGrowth"
POST 2: "New blog: 5 Ways Local AI Transforms Content Workflows in 2025. Read how Portuguese companies save €50k/year by going offline-first."
POST 3: "Limited offer: Free local AI setup consultation for Faro-based startups. DM for details. Professional, secure, unlimited."`,
leads: `Company: TechLisbon Solutions | Contact: Ana Costa (CEO) | Need: Company-wide AI deployment | Budget: €50k+ | Timeline: Q3 2025 | Employees: 85
Company: Algarve Digital Marketing | Contact: Pedro Silva (Founder) | Need: Content automation pipeline | Budget: €8k | Timeline: ASAP | Employees: 12
Company: Porto E-commerce Hub | Contact: Sofia Mendes (CTO) | Need: Customer support AI agents | Budget: €25k | Timeline: Next quarter | Employees: 45`
};
export default function Slide5Pipelines({ showToast }: { showToast: (msg: string) => void }) {
const [selectedPipeline, setSelectedPipeline] = useState<PipelineType>('triage');
const [rawText, setRawText] = useState<string>(sampleInputs.triage);
const [pipelineProcessing, setPipelineProcessing] = useState<boolean>(false);
const [results, setResults] = useState<any[] | null>(null);
const handlePipelineChange = (type: PipelineType) => {
synth.playClick();
setSelectedPipeline(type);
setRawText(sampleInputs[type]);
setResults(null);
};
const executePipeline = () => {
synth.playBlip();
setPipelineProcessing(true);
setResults(null);
setTimeout(() => {
let processedResults: any[] = [];
if (selectedPipeline === 'triage') {
const emails = rawText.split('---').filter(Boolean);
processedResults = emails.map((email, idx) => {
const lines = email.split('\n');
const fromMatch = lines.find(l => l.startsWith('From:'));
const subjectMatch = lines.find(l => l.startsWith('Subject:'));
const from = fromMatch ? fromMatch.replace('From: ', '').split('<')[0].trim() : 'Unknown';
const subject = subjectMatch ? subjectMatch.replace('Subject: ', '') : 'No subject';
const isUrgent = email.toLowerCase().includes('urgent') || email.toLowerCase().includes('asap');
const isBilling = email.toLowerCase().includes('invoice') || email.toLowerCase().includes('payment');
const isPartnership = email.toLowerCase().includes('partnership') || email.toLowerCase().includes('proposal');
const isSupport = email.toLowerCase().includes('booking') || email.toLowerCase().includes('inquiry');
let category: 'sales' | 'support' | 'billing' | 'partnership' = 'support';
if (isBilling) category = 'billing';
else if (isPartnership) category = 'partnership';
else if (isUrgent) category = 'sales';
let priority: 'critical' | 'high' | 'normal' | 'low' = 'normal';
if (isUrgent || isBilling) priority = 'critical';
else if (isPartnership) priority = 'high';
let sentiment: 'positive' | 'neutral' | 'negative' = 'neutral';
if (email.includes('Thank you') || email.includes('great')) sentiment = 'positive';
if (email.includes('Overdue') || email.includes('suspension')) sentiment = 'negative';
return {
id: `EMAIL-${String(idx + 1).padStart(3, '0')}`,
sender: from,
subject: subject,
category,
priority,
sentiment,
actionRequired: priority === 'critical' ? 'Respond within 2 hours' : 'Review within 24 hours'
};
});
} else if (selectedPipeline === 'classifier') {
const posts = rawText.split('POST').filter(Boolean);
processedResults = posts.map((post, idx) => {
const content = post.split('"')[1] || post;
const hasEmoji = /[🚀💡📈]/.test(content);
const hasHashtag = content.includes('#');
const hasLink = content.toLowerCase().includes('read') || content.toLowerCase().includes('blog');
const hasOffer = content.toLowerCase().includes('offer') || content.toLowerCase().includes('free');
let type: 'blog' | 'social' | 'email' | 'ad_copy' = 'social';
if (hasLink) type = 'blog';
if (hasOffer) type = 'ad_copy';
let tone: 'professional' | 'casual' | 'persuasive' | 'informative' = 'professional';
if (hasEmoji) tone = 'casual';
if (hasOffer) tone = 'persuasive';
if (type === 'blog') tone = 'informative';
const keywords: string[] = [];
if (content.includes('AI')) keywords.push('AI');
if (content.includes('business')) keywords.push('business');
if (content.includes('local')) keywords.push('local AI');
if (content.includes('Algarve') || content.includes('Portuguese')) keywords.push('Portugal');
return {
id: `CONTENT-${String(idx + 1).padStart(3, '0')}`,
content: content.substring(0, 80) + '...',
type,
tone,
targetAudience: type === 'social' ? 'Social media followers' : 'Business decision makers',
seoKeywords: keywords.length > 0 ? keywords : ['general']
};
});
} else {
const leads = rawText.split('\n\n').filter(Boolean);
processedResults = leads.map((lead, idx) => {
const parts: Record<string, string> = {};
lead.split(' | ').forEach(part => {
const [key, value] = part.split(': ').map(s => s.trim());
if (key && value) parts[key] = value;
});
const employeeCount = parseInt(parts['Employees'] || '0');
const budgetText = parts['Budget'] || '';
const budgetValue = parseInt(budgetText.replace(/[^0-9]/g, '')) || 0;
const isAsap = parts['Timeline']?.toLowerCase().includes('asap');
let score = 50;
if (employeeCount > 50) score += 20;
else if (employeeCount > 20) score += 10;
if (budgetValue > 30000) score += 25;
else if (budgetValue > 10000) score += 15;
if (isAsap) score += 15;
let fit: 'excellent' | 'good' | 'fair' = 'fair';
if (score >= 80) fit = 'excellent';
else if (score >= 65) fit = 'good';
let budget: 'high' | 'medium' | 'low' = 'medium';
if (budgetValue > 40000) budget = 'high';
else if (budgetValue < 15000) budget = 'low';
let timeline: 'immediate' | 'this_month' | 'quarterly' = 'quarterly';
if (isAsap) timeline = 'immediate';
else if (parts['Timeline']?.toLowerCase().includes('month')) timeline = 'this_month';
return {
id: `LEAD-${String(idx + 1).padStart(3, '0')}`,
company: parts['Company'] || 'Unknown Company',
contact: parts['Contact'] || 'Unknown',
score: Math.min(100, score),
budget,
timeline,
fit,
nextStep: score >= 80 ? 'Schedule demo this week' : score >= 65 ? 'Send follow-up email' : 'Add to nurture sequence'
};
});
}
setResults(processedResults);
setPipelineProcessing(false);
synth.playBlip();
}, 1500);
};
const handleReset = () => {
synth.playClick();
setRawText(sampleInputs[selectedPipeline]);
setResults(null);
};
const handleCopy = () => {
if (results) {
synth.playClick();
navigator.clipboard.writeText(JSON.stringify(results, null, 2));
showToast("SUCCESS // PIPELINE OUTPUT COPIED TO CLIPBOARD");
}
};
const getPriorityColor = (priority: string) => {
if (priority === 'critical') return 'bg-rose-500/20 text-rose-400 border-rose-500/30';
if (priority === 'high') return 'bg-orange-500/20 text-orange-400 border-orange-500/30';
if (priority === 'normal') return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
return 'bg-slate-500/20 text-slate-400 border-slate-500/30';
};
const getScoreColor = (score: number) => {
if (score >= 80) return 'text-emerald-400';
if (score >= 65) return 'text-yellow-400';
return 'text-orange-400';
};
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center max-w-6xl mx-auto w-full">
<div className="lg:col-span-4 flex flex-col justify-center text-left">
<span className="text-emerald-500 font-mono text-xs font-bold uppercase tracking-wider mb-2">// COGNITIVE AUTOMATION</span>
<h2 className="font-display font-bold text-2xl sm:text-3xl text-white tracking-tight uppercase leading-none mb-4">
Smart Workflow Pipelines
</h2>
<p className="text-slate-400 font-sans text-sm sm:text-base leading-relaxed mb-6">
Transform unstructured data into actionable insights. Select a pipeline type and watch local AI extract, categorize, and score your business data in milliseconds.
</p>
<div className="flex flex-col gap-3 font-mono text-[11px] text-slate-400">
<div className="flex items-center gap-2">
<span className="text-rose-500"></span>
<span>Manual sorting: 5-10 minutes per item</span>
</div>
<div className="flex items-center gap-2">
<span className="text-emerald-400"></span>
<span>Local AI pipeline: ~150ms per batch</span>
</div>
<div className="flex items-center gap-2">
<Brain className="w-3.5 h-3.5 text-emerald-400" />
<span>100% offline - zero data leaves your device</span>
</div>
</div>
</div>
<div className="lg:col-span-8 flex flex-col w-full">
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-5 sm:p-6 text-left flex flex-col gap-4">
<div className="flex items-center justify-between font-mono text-[10px] text-slate-400 mb-1">
<span>INTERACTIVE PIPELINE WORKSHOP</span>
<span className="text-emerald-400 font-bold"> LOCAL_ENGINE_ACTIVE</span>
</div>
{/* Pipeline Type Selector */}
<div className="grid grid-cols-3 gap-2 mb-4">
{pipelineOptions.map((option) => (
<button
key={option.id}
onClick={() => handlePipelineChange(option.id)}
className={`p-3 rounded-lg border text-left cursor-pointer transition-all duration-200 flex flex-col gap-1.5 ${
selectedPipeline === option.id
? 'border-emerald-500 bg-emerald-950/25 text-emerald-400 shadow-[0_0_12px_rgba(16,185,129,0.15)]'
: 'border-slate-800 bg-slate-900/40 text-slate-400 hover:border-slate-700 hover:text-slate-200'
}`}
>
<div className="flex items-center gap-2">
{option.icon}
<span className="text-[9px] font-bold tracking-tight">{option.label}</span>
</div>
<span className="text-[8px] font-light leading-tight">{option.description}</span>
</button>
))}
</div>
{/* Input Area */}
<div className="flex flex-col gap-1.5">
<label className="font-mono text-[9px] text-slate-500 uppercase">Input Data (Editable - Try modifying!):</label>
<textarea
value={rawText}
onChange={(e) => setRawText(e.target.value)}
className="bg-slate-900 border border-slate-800/80 rounded p-3 font-mono text-xs text-white h-32 focus:outline-none focus:border-emerald-500 transition-colors custom-scrollbar resize-none"
/>
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-2.5">
<button
onClick={handleReset}
className="px-3.5 py-1.5 rounded font-mono text-[10px] bg-slate-900 text-slate-400 hover:text-slate-200 border border-slate-800 transition-colors"
>
RESET SAMPLE
</button>
<button
onClick={executePipeline}
disabled={pipelineProcessing}
className={`px-4 py-1.5 rounded font-mono text-[10px] flex items-center gap-2 cursor-pointer transition-all duration-200 border ${
pipelineProcessing
? 'bg-emerald-950/20 border-emerald-500/30 text-emerald-500'
: 'bg-emerald-500 text-slate-950 border-emerald-400 hover:shadow-[0_0_12px_rgba(16,185,129,0.3)] hover:opacity-90'
}`}
>
<RefreshCw className={`w-3.5 h-3.5 ${pipelineProcessing ? 'animate-spin' : ''}`} />
{pipelineProcessing ? 'PROCESSING...' : 'RUN PIPELINE'}
</button>
</div>
{/* Results Display */}
<div className="bg-slate-900 rounded border border-slate-850 p-4 shrink-0 min-h-[200px] flex flex-col justify-center">
{pipelineProcessing ? (
<div className="flex flex-col items-center justify-center py-6 font-mono text-xs text-slate-500">
<div className="w-10 h-10 rounded-full border-2 border-t-emerald-500 border-r-transparent border-slate-800 animate-spin mb-4" />
<p className="text-emerald-400/80 animate-pulse">RUNNING LOCAL INFERENCE...</p>
<p className="text-[10px] mt-1">Extracting patterns Scoring Categorizing</p>
</div>
) : results ? (
<div className="w-full flex flex-col gap-3">
<div className="flex justify-between items-center mb-2">
<span className="font-mono text-[9px] text-emerald-400">
PROCESSED {results.length} ITEMS IN ~150MS
</span>
<button
onClick={handleCopy}
className="flex items-center gap-1.5 text-emerald-400 border border-emerald-900/35 bg-slate-950/40 hover:bg-emerald-950/20 px-2 py-0.5 rounded transition-colors text-[9px]"
>
<Copy className="w-3 h-3" />
<span>EXPORT JSON</span>
</button>
</div>
<div className="space-y-2 max-h-64 overflow-y-auto custom-scrollbar">
{results.map((item: any, idx) => (
<div key={item.id} className="bg-slate-950/50 border border-slate-800/60 rounded p-3 hover:border-emerald-500/30 transition-colors">
{selectedPipeline === 'triage' && (
<div className="flex flex-col gap-1.5">
<div className="flex justify-between items-start">
<div>
<p className="font-sans text-xs font-medium text-white">{item.sender}</p>
<p className="font-mono text-[10px] text-slate-400">{item.subject}</p>
</div>
<div className="flex gap-1">
<span className={`px-1.5 py-0.5 rounded text-[8px] border ${getPriorityColor(item.priority)}`}>
{item.priority.toUpperCase()}
</span>
<span className="px-1.5 py-0.5 rounded text-[8px] border border-slate-700 bg-slate-800/50 text-slate-400">
{item.category}
</span>
</div>
</div>
<div className="flex justify-between items-center">
<span className={`text-[9px] ${item.sentiment === 'positive' ? 'text-emerald-400' : item.sentiment === 'negative' ? 'text-rose-400' : 'text-yellow-400'}`}>
{item.sentiment === 'positive' ? '☺ Positive' : item.sentiment === 'negative' ? '⚠ Negative' : '○ Neutral'}
</span>
<span className="font-mono text-[9px] text-slate-500">{item.actionRequired}</span>
</div>
</div>
)}
{selectedPipeline === 'classifier' && (
<div className="flex flex-col gap-1.5">
<p className="font-sans text-xs text-slate-300 italic">"{item.content}"</p>
<div className="flex flex-wrap gap-1.5">
<span className="px-1.5 py-0.5 rounded text-[8px] bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">
{item.type}
</span>
<span className="px-1.5 py-0.5 rounded text-[8px] bg-blue-500/20 text-blue-400 border border-blue-500/30">
{item.tone}
</span>
<span className="px-1.5 py-0.5 rounded text-[8px] bg-slate-800/50 text-slate-400 border border-slate-700">
{item.targetAudience}
</span>
</div>
<div className="flex gap-1 flex-wrap">
{item.seoKeywords.map((kw: string, i: number) => (
<span key={i} className="text-[8px] text-emerald-500/70">#{kw}</span>
))}
</div>
</div>
)}
{selectedPipeline === 'leads' && (
<div className="flex justify-between items-center">
<div className="flex flex-col gap-1">
<p className="font-sans text-xs font-bold text-white">{item.company}</p>
<p className="font-mono text-[10px] text-slate-400">{item.contact}</p>
<div className="flex gap-2 text-[9px]">
<span className="text-slate-500">Budget: <span className={item.budget === 'high' ? 'text-emerald-400' : 'text-slate-400'}>{item.budget}</span></span>
<span className="text-slate-500">Timeline: <span className={item.timeline === 'immediate' ? 'text-emerald-400' : 'text-slate-400'}>{item.timeline.replace('_', ' ')}</span></span>
</div>
</div>
<div className="text-right">
<div className={`text-2xl font-bold ${getScoreColor(item.score)}`}>{item.score}</div>
<div className="text-[9px] text-slate-500">LEAD SCORE</div>
<div className={`text-[9px] mt-1 ${item.fit === 'excellent' ? 'text-emerald-400' : item.fit === 'good' ? 'text-yellow-400' : 'text-orange-400'}`}>
{item.fit.toUpperCase()} FIT
</div>
</div>
</div>
)}
</div>
))}
</div>
</div>
) : (
<div className="text-center font-mono py-8 text-slate-500">
<Brain className="w-12 h-12 mx-auto mb-3 text-slate-600" />
<p className="text-xs mb-1">PIPELINE STANDBY</p>
<p className="text-[10px] font-sans">Select a pipeline type above and click "RUN PIPELINE" to process data locally.</p>
</div>
)}
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,322 @@
import { useState } from 'react';
import { Cpu, Sparkles, ArrowRight } from 'lucide-react';
import { BudgetType, Slide6Tab, GoalType } from '../../types';
import { synth } from '../../utils/audioSynth';
interface HardwareRow {
id: number;
tier: string;
hardware: string;
models: string;
capacity: string;
}
const hardwareRows: HardwareRow[] = [
{
id: 0,
tier: "Baseline: 16GB RAM",
hardware: "Office Laptops (M1/M2/M3 Macbook Air, Intel Core i7 with integrated Iris)",
models: "Llama 3 8B, Qwen 2.5 7B, Mistral 7B (Q4_K_M)",
capacity: "Low Latency Single Task"
},
{
id: 1,
tier: "Sweet Spot: 32GB-48GB RAM",
hardware: "Developer Workstations (Macbook Pro M3 Max, Nvidia RTX 4070/4080 Super)",
models: "Qwen 2.5 14B/32B, Phi-3-Medium, Command R",
capacity: "Multi-Agent Pipelines & Full RAG"
},
{
id: 2,
tier: "Enterprise: 64GB+ RAM",
hardware: "In-House Server Nodes (Mac Studio, Dual NVIDIA RTX 4090, Host Workstations)",
models: "Llama 3 70B (Q4), Mixtral 8x22B Mixture of Experts",
capacity: "Parallel Enterprise API Replacement"
}
];
const hardwareDetails: Record<number, string> = {
0: "BASELINE CAPABILITY: Perfect for running 3B-8B parameters quantized models at peak speed. Integrates simple office assistants, direct translation pipelines, and localized structured search indexing without lags.",
1: "HIGH PERFORMANCE (SWEET SPOT): Enables 14B-32B parameters models with complex reasoning, basic RAG, data pipeline execution, multi-stage summarization. Highly responsive on desktop workstations (Mac Studio / Nvidia GeForce RTX).",
2: "ENTERPRISE DEPLOYMENTS: Capable of running heavy 70B+ parameters sovereign engines or multiple concurrent agent pipelines. Replaces complex proprietary SaaS platforms completely with extreme performance."
};
const goalVerdicts: Record<GoalType, string> = {
writing: "PERFECTLY FEASIBLE: Writing assistance is extremely light on RAM. A standard 16GB office laptop can handle this effortlessly, outputting texts faster than human typing with zero cloud lag.",
rag: "HIGHLY PRACTICAL: Deep content classification and table mapping excel in dedicated local structures. No sensitive customer directories are ever sent across cloud channels.",
agents: "REVENUE WINNER: Drag-and-drop secure file indexing allows instant conversational searches of massive internal manuals. Keeps sensitive IP completely under your company lock and key."
};
interface TaskSpec {
model: string;
setup: string;
hardware: string;
cost: string;
}
const getTaskSpec = (goal: GoalType): TaskSpec => {
const specs: Record<GoalType, TaskSpec> = {
writing: {
model: "Llama-3.1-8B-Instruct (or Mistral-7B)",
setup: "LM Studio / Jan Desktop (GUI offline shells)",
hardware: "8GB-16GB RAM laptop or consumer PC",
cost: "$0.00 / Zero subscription forever"
},
rag: {
model: "Qwen-2.5-14B-Instruct-Q4 (or Llama-3-8B)",
setup: "AnythingLLM / Open WebUI Desktop GUI (Zero coding required)",
hardware: "16GB RAM laptop or light business machine",
cost: "$0.00 / Zero subscription forever"
},
agents: {
model: "Command-R-35B-Q4 (or Llama-3.1-8B-Instruct)",
setup: "AnythingLLM / PrivateGPT (Desktop Drag-and-Drop)",
hardware: "16GB-32GB standard office PC (Unified RAM)",
cost: "$0.00 / Zero subscription forever"
}
};
return specs[goal];
};
interface ActionStep {
num: number;
text: string;
}
const getActionSteps = (goal: GoalType): ActionStep[] => {
if (goal === 'writing') {
return [
{ num: 1, text: "Download & install <strong>LM Studio</strong> or <strong>Jan App</strong> on your current hardware." },
{ num: 2, text: "Search registry for <strong>llama-3.1-8b-instruct</strong> and download the 4-bit config weight (Q4_K_M)." },
{ num: 3, text: "You're ready! Start writing content safely offline with zero licensing or subscription stress!" }
];
} else if (goal === 'rag') {
return [
{ num: 1, text: "Download & install <strong>AnythingLLM Desktop</strong> or <strong>Jan Desktop</strong>." },
{ num: 2, text: "Launch the app, choose any small quantized model from the built-in library, and click download." },
{ num: 3, text: "Drag and drop your content folders or business contracts to automatically organize, search, or summarize them!" }
];
} else {
return [
{ num: 1, text: "Download & launch the free <strong>AnythingLLM Desktop</strong> program." },
{ num: 2, text: "Establish a localized workspace and import folder directories of PDF reports/Excel briefs." },
{ num: 3, text: "Query models securely offline. The vector database runs completely on disk with zero cloud lookups." }
];
}
};
interface Slide6HardwareMatrixProps {
selectedGoal: GoalType;
procureBudget: BudgetType;
onGoalChange: (goal: GoalType) => void;
onBudgetChange: (budget: BudgetType) => void;
}
export default function Slide6HardwareMatrix({
selectedGoal,
procureBudget,
onGoalChange,
onBudgetChange
}: Slide6HardwareMatrixProps) {
const [selectedHardwareRow, setSelectedHardwareRow] = useState<number>(0);
const [slide6Tab, setSlide6Tab] = useState<Slide6Tab>('tasks');
const handleElementClick = () => synth.playClick();
const handleTabChange = (tab: Slide6Tab) => {
handleElementClick();
setSlide6Tab(tab);
};
const handleHardwareSelect = (id: number) => {
handleElementClick();
setSelectedHardwareRow(id);
};
const handleGoalSelect = (goal: GoalType) => {
handleElementClick();
onGoalChange(goal);
};
const taskSpec = getTaskSpec(selectedGoal);
const actionSteps = getActionSteps(selectedGoal);
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-stretch max-w-6xl mx-auto w-full text-left">
<div className="lg:col-span-5 flex flex-col justify-between">
<div>
<span className="text-emerald-500 font-mono text-xs font-bold uppercase tracking-wider mb-2">// DIRECTIVE ARCHITECTURE</span>
<h2 className="font-display font-bold text-2xl sm:text-3xl text-white tracking-tight uppercase leading-none mb-4">
Your Hardware Match
</h2>
<p className="text-slate-400 font-sans text-sm sm:text-base leading-relaxed mb-6">
Map local AI targets against your company assets. Standard office equipment already houses substantial computational power. Utilize our selector to plan your deployment.
</p>
<div className="flex bg-slate-950/80 border border-slate-800/80 rounded p-1 gap-1 mb-6 font-mono text-[10px]">
<button
type="button"
onClick={() => handleTabChange('tasks')}
className={`flex-1 py-2 px-3 rounded text-center transition-all duration-150 cursor-pointer font-bold ${
slide6Tab === 'tasks'
? 'bg-emerald-950/50 border border-emerald-500/30 text-emerald-400 text-glow'
: 'text-slate-500 hover:text-slate-350 border border-transparent'
}`}
>
USE CASES MATCHING
</button>
<button
type="button"
onClick={() => handleTabChange('hardware')}
className={`flex-1 py-2 px-3 rounded text-center transition-all duration-150 cursor-pointer font-bold ${
slide6Tab === 'hardware'
? 'bg-emerald-950/50 border border-emerald-500/30 text-emerald-400 text-glow'
: 'text-slate-500 hover:text-slate-350 border border-transparent'
}`}
>
💻 HARDWARE TIERS
</button>
</div>
</div>
{slide6Tab === 'hardware' ? (
<div className="bg-slate-950/40 border border-emerald-950/30 rounded-lg p-5 font-mono">
<div className="flex items-center gap-2 text-emerald-400 text-xs font-bold mb-2">
<Cpu className="w-4 h-4 text-emerald-400" />
<span>MAPPED SILICON COMPATIBILITY:</span>
</div>
<p className="text-[12px] leading-relaxed text-slate-300">
{hardwareDetails[selectedHardwareRow]}
</p>
</div>
) : (
<div className="bg-slate-950/40 border border-emerald-950/30 rounded-lg p-5 font-mono">
<div className="flex items-center gap-2 text-emerald-400 text-xs font-bold mb-2">
<Sparkles className="w-4 h-4 text-emerald-400" />
<span>SOVEREIGN VERDICT:</span>
</div>
<p className="text-[12px] leading-relaxed text-slate-300">
{goalVerdicts[selectedGoal]}
</p>
</div>
)}
</div>
<div className="lg:col-span-7 flex flex-col w-full">
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-5 sm:p-6 flex-1 flex flex-col justify-between relative overflow-hidden">
{slide6Tab === 'hardware' && (
<div className="flex flex-col gap-3">
<div className="flex justify-between items-center font-mono text-[10px] text-slate-400 mb-2">
<span>INTERACTIVE MEMORY SPEC MATRIX</span>
<span className="text-emerald-400 font-bold"> SELECT HARDWARE</span>
</div>
{hardwareRows.map((row) => (
<div
key={row.id}
onClick={() => handleHardwareSelect(row.id)}
className={`p-4 rounded-lg border transition-all duration-300 cursor-pointer flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 ${
selectedHardwareRow === row.id
? 'border-emerald-500 bg-emerald-950/25 shadow-[0_0_15px_rgba(16,185,129,0.15)]'
: 'border-slate-850 bg-slate-900/40 hover:border-slate-700 hover:bg-slate-900/80'
}`}
>
<div className="text-left">
<span className={`font-mono text-[9px] font-bold block mb-1 uppercase ${
selectedHardwareRow === row.id ? 'text-emerald-400' : 'text-slate-500'
}`}>
TIER_0{row.id + 1} // {selectedHardwareRow === row.id ? 'ACTIVE SPEC' : 'INACTIVE'}
</span>
<h3 className="font-display font-bold text-white text-base">
{row.tier}
</h3>
<p className="font-sans text-[11px] text-slate-400 mt-0.5">
<strong>Hardware:</strong> {row.hardware}
</p>
</div>
<div className="text-left sm:text-right shrink-0">
<span className="font-mono text-[10px] text-emerald-500 text-glow font-bold block">
{row.models}
</span>
<span className="font-sans text-[10px] text-slate-500 block">
{row.capacity}
</span>
</div>
</div>
))}
</div>
)}
{slide6Tab === 'tasks' && (
<div className="flex flex-col h-full justify-between">
<div className="flex justify-between items-center font-mono text-[10px] text-slate-400 mb-4">
<span>TASK TO LOCAL MODEL MATCHING BLUEPRINTS</span>
<span className="text-emerald-400 font-bold"> ONLINE PLANNER</span>
</div>
<div className="grid grid-cols-3 gap-2 mb-4 font-mono">
{[
{ id: 'writing' as GoalType, label: '✍ WRITING SUPPORT', desc: 'Content & copywriting' },
{ id: 'rag' as GoalType, label: '📄 DOC ANALYSIS', desc: 'Data parsing & extraction' },
{ id: 'agents' as GoalType, label: '🔍 FILE Q&A (RAG)', desc: 'Knowledge Base search' }
].map((tsk) => (
<button
key={tsk.id}
onClick={() => handleGoalSelect(tsk.id)}
className={`p-2.5 rounded border text-center cursor-pointer transition-all duration-200 flex flex-col items-center justify-center gap-1 ${
selectedGoal === tsk.id
? 'border-emerald-500 bg-emerald-950/25 text-emerald-400 font-bold shadow-[0_0_8px_rgba(16,185,129,0.1)]'
: 'border-slate-850 bg-slate-900/40 text-slate-400 hover:border-slate-700 hover:text-slate-200'
}`}
>
<span className="text-[10px] font-bold leading-none">{tsk.label}</span>
<span className="text-[7.5px] text-slate-500 leading-none">{tsk.desc}</span>
</button>
))}
</div>
<div className="bg-slate-900 border border-slate-850 rounded p-4 mb-4 grid grid-cols-1 md:grid-cols-2 gap-4 text-slate-200 text-xs font-mono">
<div className="flex flex-col gap-2">
<div>
<span className="text-[9px] text-slate-500 font-semibold block uppercase">TARGET MODEL WEIGHTS:</span>
<span className="text-emerald-400 font-bold text-sm">{taskSpec.model}</span>
</div>
<div>
<span className="text-[9px] text-slate-500 font-semibold block uppercase">PRACTICAL SETUP APPLICATION:</span>
<span className="text-white">{taskSpec.setup}</span>
</div>
</div>
<div className="flex flex-col gap-2 md:border-l md:border-slate-800 md:pl-4">
<div>
<span className="text-[9px] text-slate-500 font-semibold block uppercase">MINIMUM COMPUTER HARDWARE:</span>
<span className="text-white">{taskSpec.hardware}</span>
</div>
<div>
<span className="text-[9px] text-slate-500 font-semibold block uppercase">COST TAX:</span>
<span className="text-emerald-400 text-glow font-bold">{taskSpec.cost}</span>
</div>
</div>
</div>
<div className="bg-slate-950 rounded p-3 text-left font-mono border border-slate-850">
<span className="text-[8.5px] text-slate-500 font-bold block uppercase mb-1.5">// IMMEDIATE ACTION PLAN (FIRST STEPS):</span>
<div className="flex flex-col gap-1.5 text-[11px] text-slate-300">
{actionSteps.map((step) => (
<p key={step.num} className="flex gap-2">
<span className="text-emerald-400 font-bold">{step.num}.</span>
<span dangerouslySetInnerHTML={{ __html: step.text }} />
</p>
))}
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
}
@@ -0,0 +1,210 @@
import { AlertTriangle, Terminal, Sparkles, ArrowRight } from 'lucide-react';
import { BudgetType, GoalType } from '../../types';
import { getHardwareProcureData } from '../../utils/hardwareData';
import { synth } from '../../utils/audioSynth';
interface Slide7BuyersGuideProps {
showToast: (msg: string) => void;
selectedGoal: GoalType;
procureBudget: BudgetType;
onGoalChange: (goal: GoalType) => void;
onBudgetChange: (budget: BudgetType) => void;
}
interface BudgetOption {
id: BudgetType;
label: string;
sub: string;
}
const budgetOptions: BudgetOption[] = [
{ id: 'existing', label: '💻 EXISTING ASSETS', sub: 'Zero Cost ($0)' },
{ id: 'budget', label: '⚡ LIGHT UPGRADE', sub: 'Under $1,500' },
{ id: 'pro', label: '🔥 PRO STUDIO', sub: '$2k - $4k' },
{ id: 'enterprise', label: '🏢 ENTERPRISE NODE', sub: 'Over $5k' }
];
export default function Slide7BuyersGuide({
showToast,
selectedGoal,
procureBudget,
onGoalChange,
onBudgetChange
}: Slide7BuyersGuideProps) {
const handleElementClick = () => synth.playClick();
const handleBudgetSelect = (budget: BudgetType) => {
handleElementClick();
onBudgetChange(budget);
};
const handleGoalSelect = (goal: GoalType) => {
handleElementClick();
onGoalChange(goal);
};
const spec = getHardwareProcureData(procureBudget, selectedGoal);
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-stretch max-w-6xl mx-auto w-full text-left font-sans animate-fade-in">
<div className="lg:col-span-5 flex flex-col justify-between">
<div>
<span className="text-emerald-500 font-mono text-xs font-bold uppercase tracking-wider mb-2 block">// SPECIFICATION OPTIMIZER</span>
<h2 className="font-display font-bold text-2xl sm:text-3xl text-white tracking-tight uppercase leading-none mb-4">
Easy Setup & Hardware Guide
</h2>
<p className="text-slate-400 text-xs sm:text-sm leading-relaxed mb-5">
Buying or repurposing hardware is simple once you isolate your goals. Modern large models scale entirely based on **Memory Bandwidth** rather than CPU cores. Select your options below to see your optimal setup plan:
</p>
<div className="mb-4">
<label className="block text-slate-500 font-mono text-[9px] uppercase tracking-wider mb-2 font-bold">// 1. CHOOSE AVAILABLE BUDGET PROFILE</label>
<div className="grid grid-cols-2 gap-2 font-mono">
{budgetOptions.map((btn) => (
<button
key={btn.id}
type="button"
onClick={() => handleBudgetSelect(btn.id)}
className={`p-2.5 rounded border text-left transition-all duration-200 cursor-pointer ${
procureBudget === btn.id
? 'border-emerald-500 bg-emerald-950/20 text-emerald-400 font-bold shadow-[0_0_8px_rgba(16,185,129,0.1)]'
: 'border-slate-850 bg-slate-900/30 text-slate-400 hover:border-slate-750 hover:text-slate-200'
}`}
>
<span className="block text-[10px] tracking-tight">{btn.label}</span>
<span className={`block text-[8px] font-semibold ${procureBudget === btn.id ? 'text-emerald-500' : 'text-slate-500'}`}>{btn.sub}</span>
</button>
))}
</div>
</div>
<div className="mb-5">
<label className="block text-slate-500 font-mono text-[9px] uppercase tracking-wider mb-2 font-bold">// 2. CHOOSE PRIMARY BUSINESS VALUE INTENT</label>
<div className="flex flex-col gap-2 font-mono">
<button
type="button"
onClick={() => handleGoalSelect('writing')}
className={`p-3 rounded border text-left transition-all duration-150 cursor-pointer flex justify-between items-center ${
selectedGoal === 'writing'
? 'border-emerald-500 bg-emerald-950/25 text-emerald-400 font-bold'
: 'border-slate-850 bg-slate-900/30 text-slate-400 hover:border-slate-750 hover:text-slate-200'
}`}
>
<div>
<span className="block text-[10px] tracking-tight"> CONTENT ENGINE</span>
<span className="block font-sans text-[8.5px] font-normal text-slate-500 leading-none mt-0.5">Drafting, summarizing, code autocompletion</span>
</div>
<ArrowRight className={`w-3 h-3 shrink-0 ${selectedGoal === 'writing' ? 'text-emerald-400' : 'text-slate-600'}`} />
</button>
<button
type="button"
onClick={() => handleGoalSelect('rag')}
className={`p-3 rounded border text-left transition-all duration-150 cursor-pointer flex justify-between items-center ${
selectedGoal === 'rag'
? 'border-emerald-500 bg-emerald-950/25 text-emerald-400 font-bold'
: 'border-slate-850 bg-slate-900/30 text-slate-400 hover:border-slate-750 hover:text-slate-200'
}`}
>
<div>
<span className="block text-[10px] tracking-tight">🔍 FILE COGNITIVE INDEX (RAG)</span>
<span className="block font-sans text-[8.5px] font-normal text-slate-500 leading-none mt-0.5">Instant Q&A across deep manual archives / directories</span>
</div>
<ArrowRight className={`w-3 h-3 shrink-0 ${selectedGoal === 'rag' ? 'text-emerald-400' : 'text-slate-600'}`} />
</button>
<button
type="button"
onClick={() => handleGoalSelect('agents')}
className={`p-3 rounded border text-left transition-all duration-150 cursor-pointer flex justify-between items-center ${
selectedGoal === 'agents'
? 'border-emerald-500 bg-emerald-950/25 text-emerald-400 font-bold'
: 'border-slate-850 bg-slate-900/30 text-slate-400 hover:border-slate-750 hover:text-slate-200'
}`}
>
<div>
<span className="block text-[10px] tracking-tight">🤖 MULTI-AGENT SWARM PIPELINES</span>
<span className="block font-sans text-[8.5px] font-normal text-slate-500 leading-none mt-0.5">Host complex orchestrated background reasoning routines</span>
</div>
<ArrowRight className={`w-3 h-3 shrink-0 ${selectedGoal === 'agents' ? 'text-emerald-400' : 'text-slate-600'}`} />
</button>
</div>
</div>
</div>
<div className="bg-rose-950/15 border border-rose-900/25 p-4 rounded-lg text-slate-350 font-sans text-[11px] leading-relaxed flex gap-3">
<AlertTriangle className="w-5 h-5 text-rose-500 shrink-0 animate-pulse" />
<div>
<span className="font-mono text-[9.5px] font-bold text-rose-400 tracking-wider block uppercase mb-1">// CRITICAL BUYER SHIELD PROTOCOL:</span>
Do NOT buy multiple CPU-core servers expecting quick inference speeds. LLM read-phases are bound strictly by **Memory Bandwidth**. Prefer Apple Silicon (Unified Memory bus width &gt; 300GB/s) or discrete Nvidia RTX Series GPUs. CPU-only rendering will result in a 5x to 10x speed penalty.
</div>
</div>
</div>
<div className="lg:col-span-7 flex flex-col w-full font-sans">
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-5 sm:p-6 flex-1 flex flex-col justify-between relative overflow-hidden text-slate-200 font-mono">
<div className="flex justify-between items-center font-mono text-[10px] text-slate-400 mb-4 border-b border-slate-900 pb-3">
<span>SOVEREIGN HARDWARE DEPLOYMENT SPECIFICATION</span>
<span className="text-emerald-400 font-bold animate-pulse"> MODEL GENERATED</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 text-left">
<div className="p-3 bg-slate-900/80 border border-slate-850 rounded">
<span className="text-[8.5px] text-slate-500 block font-semibold">// RECOMMENDED SILICON TYPE:</span>
<span className="text-white text-xs font-bold block mt-1 leading-normal font-sans">{spec.machineType}</span>
</div>
<div className="p-3 bg-slate-900/80 border border-slate-850 rounded">
<span className="text-[8.5px] text-slate-500 block font-semibold">// OPTIMAL CAPACITY & MEMORY:</span>
<span className="text-emerald-400 text-glow text-xs font-bold block mt-1 leading-normal font-sans">{spec.ramType}</span>
</div>
<div className="p-3 bg-slate-900/80 border border-slate-850 rounded">
<span className="text-[8.5px] text-slate-500 block font-semibold">// MINIMUM SYSTEM BUS SPEED:</span>
<span className="text-white text-xs block mt-1 leading-normal font-sans">{spec.bandWidth}</span>
</div>
<div className="p-3 bg-slate-900/80 border border-slate-850 rounded">
<span className="text-[8.5px] text-slate-500 block font-semibold">// TARGET QUANTIZED MODEL WEIGHTS:</span>
<span className="text-emerald-400 font-bold text-xs block mt-1 leading-normal font-sans">{spec.modelGoal}</span>
</div>
</div>
<div className="bg-slate-900 border border-slate-850 rounded p-4 mb-4 text-left font-mono">
<div className="flex items-center gap-1.5 text-emerald-450 text-[10px] font-bold mb-1.5 uppercase">
<Sparkles className="w-3.5 h-3.5 text-emerald-400" />
<span>PERFORMANCE CAPABILITY ESTIMATE:</span>
</div>
<p className="text-slate-300 text-xs font-sans leading-relaxed">
{spec.verdict}
</p>
</div>
<div className="bg-slate-950 p-4 rounded-lg border border-slate-850 mb-4 text-left text-xs">
<span className="text-[9px] text-slate-500 font-bold block uppercase mb-2 font-mono">// SOFTWARE STACK TO DEPLOY:</span>
<div className="flex gap-2 items-start text-slate-300 bg-slate-900/50 p-2.5 rounded border border-slate-850">
<Terminal className="w-4 h-4 text-emerald-400 shrink-0 mt-0.5" />
<div className="font-sans">
<strong className="text-white block text-[11px] mb-0.5 font-mono">// LAUNCH TOOL COMPANIONS</strong>
<p className="text-[10px] text-slate-400 leading-relaxed mb-1.5">
Download & initialize <span className="text-emerald-400 font-bold">{spec.setupTools}</span>.
</p>
<span className="text-[9.5px] text-slate-500 italic block">
{spec.purchaseTip}
</span>
</div>
</div>
</div>
<div className="bg-slate-900/40 p-3 rounded text-[9px] text-slate-500 flex justify-between items-center border border-slate-900 font-mono">
<span className="flex items-center gap-1"><span className="text-emerald-500"></span> SOURCE SPEC</span>
<span className="text-slate-700"></span>
<span className="flex items-center gap-1"><span className="text-emerald-500"></span> MAP TO GOALS</span>
<span className="text-slate-700"></span>
<span className="flex items-center gap-1"><span className="text-emerald-500"></span> BYPASS CPU TRAP</span>
<span className="text-slate-700"></span>
<span className="text-emerald-400 font-bold animate-pulse">// REQUISITION READY</span>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,219 @@
import { useState } from 'react';
import { GoalType, BudgetType } from '../../types';
import { synth } from '../../utils/audioSynth';
interface Slide8ROIProps {
selectedGoal: GoalType;
procureBudget: BudgetType;
showToast: (msg: string) => void;
}
export default function Slide8ROI({ selectedGoal, procureBudget, showToast }: Slide8ROIProps) {
const [employees, setEmployees] = useState<number>(120);
const [monthlySaaS, setMonthlySaaS] = useState<number>(35);
const [setupCosts, setSetupCosts] = useState<number>(6500);
const handleElementClick = () => synth.playClick();
const handleGoalCostApply = () => {
handleElementClick();
const goalCostMap: Record<GoalType, number> = { writing: 0, rag: 1500, agents: 3500 };
const targetCost = goalCostMap[selectedGoal] ?? 3500;
setSetupCosts(targetCost);
showToast(`Applied ${selectedGoal.toUpperCase()} goal recommendation: $${targetCost.toLocaleString()}`);
};
const handleBudgetCostApply = () => {
handleElementClick();
const bgtCostMap: Record<BudgetType, number> = { existing: 0, budget: 1500, pro: 3500, enterprise: 12500 };
const targetCost = bgtCostMap[procureBudget] ?? 1500;
setSetupCosts(targetCost);
showToast(`Applied ${procureBudget.toUpperCase()} specification plan: $${targetCost.toLocaleString()}`);
};
const cloudMonthlyCost = employees * monthlySaaS;
const cloudAnnualCost = cloudMonthlyCost * 12;
const localAnnualOperatingSalaryFraction = 1200;
const firstYearSavings = Math.max(0, cloudAnnualCost - setupCosts - localAnnualOperatingSalaryFraction);
const threeYearSavings = Math.max(0, (cloudAnnualCost * 3) - setupCosts - (localAnnualOperatingSalaryFraction * 3));
const breakEvenMonths = cloudMonthlyCost > 0 ? (setupCosts / cloudMonthlyCost).toFixed(1) : "0.0";
const getGoalCost = () => {
const goalCostMap: Record<GoalType, number> = { writing: 0, rag: 1500, agents: 3500 };
return goalCostMap[selectedGoal];
};
const getBudgetCost = () => {
const bgtCostMap: Record<BudgetType, number> = { existing: 0, budget: 1500, pro: 3500, enterprise: 12500 };
return bgtCostMap[procureBudget];
};
const getGoalLabel = () => {
if (selectedGoal === 'writing') return "✍ Writing Support";
if (selectedGoal === 'rag') return "📄 Doc Analysis";
return "🔍 File Q&A (RAG)";
};
const getBudgetLabel = () => {
if (procureBudget === 'existing') return "💻 Existing Machine";
if (procureBudget === 'budget') return "⚡ Light Upgrade";
if (procureBudget === 'pro') return "🔥 Pro Studio Setup";
return "🏢 Enterprise Rack";
};
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center max-w-6xl mx-auto w-full">
<div className="lg:col-span-5 flex flex-col justify-center text-left">
<span className="text-emerald-500 font-mono text-xs font-bold uppercase tracking-wider mb-2">// COST BREAKDOWN</span>
<h2 className="font-display font-bold text-2xl sm:text-3xl text-white tracking-tight uppercase leading-none mb-4">
Zero-Cost Lifetime ROI
</h2>
<p className="text-slate-400 font-sans text-sm sm:text-base leading-relaxed mb-6">
SaaS applications levy a lifetime licensing tax. Leverage your existing compute hardware assets to entirely sever monthly billing spikes and dynamically scale model invocations with absolute zero marginal cost.
</p>
<div className="bg-slate-950/50 border border-slate-800/80 rounded-xl p-5 font-mono text-[11px] text-slate-350 flex flex-col gap-2.5">
<div className="flex items-center justify-between border-b border-slate-850 pb-2 text-[10px] text-slate-500">
<span>SOVEREIGN EFFICIENCY REPORT</span>
<span className="text-emerald-400">STATUS: GOLD_RATED</span>
</div>
<div className="flex justify-between">
<span>ESTIMATED BREAK-EVEN:</span>
<span className="text-emerald-400 text-glow font-bold">{breakEvenMonths} Months</span>
</div>
<div className="flex justify-between">
<span>FIRST YEAR SAVINGS:</span>
<span className="text-emerald-400 font-bold">${firstYearSavings.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span>3-YEAR LIFETIME SAVINGS:</span>
<span className="text-emerald-400 text-glow font-bold">${threeYearSavings.toLocaleString()}</span>
</div>
</div>
</div>
<div className="lg:col-span-7 flex flex-col w-full">
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-6 text-left flex flex-col gap-5">
<div className="flex justify-between items-center font-mono text-[10px] text-slate-400">
<span>LIVE RETURN-ON-INVESTMENT CALCULATOR</span>
<span className="text-emerald-400 font-bold"> DYNAMIC</span>
</div>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between font-mono text-[11px]">
<span className="text-slate-300">COMPANY SIZE (STAFF/USERS):</span>
<span className="text-white font-bold">{employees} employees</span>
</div>
<input
type="range"
min="10"
max="500"
step="10"
value={employees}
onChange={(e) => { handleElementClick(); setEmployees(Number(e.target.value)); }}
className="w-full h-1 bg-slate-900 rounded-lg appearance-none cursor-pointer accent-emerald-500"
/>
</div>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between font-mono text-[11px]">
<span className="text-slate-300">ESTIMATED MONTHLY CLOUD SAAS COST (PER USER):</span>
<span className="text-white font-bold">${monthlySaaS} / user / mo</span>
</div>
<input
type="range"
min="10"
max="100"
step="5"
value={monthlySaaS}
onChange={(e) => { handleElementClick(); setMonthlySaaS(Number(e.target.value)); }}
className="w-full h-1 bg-slate-900 rounded-lg appearance-none cursor-pointer accent-emerald-500"
/>
</div>
<div className="bg-slate-900/60 p-3 rounded-lg border border-slate-800/80 flex flex-col gap-3 font-sans">
<div className="flex justify-between items-center text-[10px] font-mono font-bold text-slate-400 uppercase tracking-tight">
<span>🔌 SYNC INFRASTRUCTURE COST TO DECISION PATH</span>
<span className="text-emerald-400 font-black tracking-normal animate-pulse"> SELECT TO APPLY</span>
</div>
<div className="grid grid-cols-2 gap-2.5">
<button
type="button"
onClick={handleGoalCostApply}
className="p-2.5 rounded border border-slate-850 bg-slate-950/40 text-left hover:border-emerald-500/50 hover:bg-slate-900/80 hover:shadow-[0_0_12px_rgba(16,185,129,0.06)] transition-all duration-200 cursor-pointer flex flex-col justify-between"
>
<div>
<span className="text-[8px] text-slate-500 font-mono tracking-wider font-bold block uppercase mb-1">// S6 ACTIVE GOAL</span>
<span className="text-[11px] text-white font-bold leading-none font-sans block truncate">
{getGoalLabel()}
</span>
</div>
<span className="text-[10px] text-emerald-400 font-bold block mt-2 font-mono">
Use Rec: ${getGoalCost().toLocaleString()}
</span>
</button>
<button
type="button"
onClick={handleBudgetCostApply}
className="p-2.5 rounded border border-slate-850 bg-slate-950/40 text-left hover:border-emerald-500/50 hover:bg-slate-900/80 hover:shadow-[0_0_12px_rgba(16,185,129,0.06)] transition-all duration-200 cursor-pointer flex flex-col justify-between"
>
<div>
<span className="text-[8px] text-slate-500 font-mono tracking-wider font-bold block uppercase mb-1">// S7 SPECIFICATION</span>
<span className="text-[11px] text-white font-bold leading-none font-sans block truncate">
{getBudgetLabel()}
</span>
</div>
<span className="text-[10px] text-emerald-450 font-bold block mt-2 font-mono">
Spec Cost: ${getBudgetCost().toLocaleString()}
</span>
</button>
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between font-mono text-[11px]">
<span className="text-slate-300">ONE-OFF INFRASTRUCTURE UPGRADE INVESTMENT:</span>
<span className="text-white font-bold">${setupCosts.toLocaleString()}</span>
</div>
<input
type="range"
min="0"
max="30000"
step="500"
value={setupCosts}
onChange={(e) => { handleElementClick(); setSetupCosts(Number(e.target.value)); }}
className="w-full h-1 bg-slate-900 rounded-lg appearance-none cursor-pointer accent-emerald-500"
/>
</div>
<div className="grid grid-cols-2 gap-4 mt-2">
<div className="p-4 rounded-lg bg-slate-905 border border-slate-850 flex flex-col justify-between">
<div>
<span className="font-mono text-[9px] text-slate-500 block uppercase mb-1">Yearly Cloud Cost (Hemorrhaging)</span>
<span className="font-display font-extrabold text-lg sm:text-xl text-rose-450">${cloudAnnualCost.toLocaleString()}</span>
</div>
<div className="h-2 w-full bg-slate-950 rounded-full overflow-hidden mt-3">
<div className="h-full bg-rose-500 w-full" />
</div>
</div>
<div className="p-4 rounded-lg bg-emerald-950/20 border border-emerald-900/35 flex flex-col justify-between">
<div>
<span className="font-mono text-[9px] text-emerald-450 block uppercase mb-1">Local Sovereign Cost (First Year Upgrade)</span>
<span className="font-display font-extrabold text-lg sm:text-xl text-emerald-450">${(setupCosts + 1200).toLocaleString()}</span>
</div>
<div className="h-2 w-full bg-slate-950 rounded-full overflow-hidden mt-3">
<div
className="h-full bg-emerald-500 transition-all duration-300"
style={{ width: `${Math.min(100, ((setupCosts + 1200) / cloudAnnualCost) * 100)}%` }}
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,196 @@
import { useState, useRef, FormEvent } from 'react';
import { motion } from 'motion/react';
import { WifiOff, Lock } from 'lucide-react';
import { LogSequence } from '../../types';
import { synth } from '../../utils/audioSynth';
interface Slide9OfflineDemoProps {
showToast: (msg: string) => void;
}
export default function Slide9OfflineDemo({ showToast }: Slide9OfflineDemoProps) {
const [isDisconnected, setIsDisconnected] = useState<boolean>(false);
const [demoStep, setDemoStep] = useState<number>(0);
const [demoLogs, setDemoLogs] = useState<string[]>([]);
const [demoPrompt, setDemoPrompt] = useState<string>("");
const [demoResponse, setDemoResponse] = useState<string>("");
const [demoPending, setDemoPending] = useState<boolean>(false);
const demoLogTimerRef = useRef<NodeJS.Timeout[]>([]);
const handleSeverConnection = () => {
synth.playDisconnect();
setIsDisconnected(true);
setDemoStep(1);
setDemoLogs([]);
setDemoResponse("");
setDemoPrompt("");
demoLogTimerRef.current.forEach(t => clearTimeout(t));
demoLogTimerRef.current = [];
const logSequence: LogSequence[] = [
{ text: "⚡ [SEVER_INITIATED] Intercepting all global socket calls...", delay: 200 },
{ text: "🔴 [PING_FAILED] api.openai.com - Request timeout (Route blocked)", delay: 800 },
{ text: "🔴 [CONN_DEAD] gateway.anthropic.com - Destination unreachable", delay: 1400 },
{ text: "🔒 [OFFLINE_ENFORCED] Firewall rules securely locked. Outbound data leakage risk: 0.00%", delay: 2000 },
{ text: "📦 [DEPLOYING] Mount local weights: 'llama-3-8b-instruct.Q4_K_M.gguf'", delay: 2600 },
{ text: "🚀 [CPU_AVX2] GPU acceleration mapped to RTX VRAM Core.", delay: 3200 },
{ text: "✅ [Sovereign-Llama3-v1] Online in 100% disconnected sandbox environment.", delay: 3800 }
];
logSequence.forEach((seq) => {
const timer = setTimeout(() => {
setDemoLogs((prev) => [...prev, seq.text]);
synth.playBlip();
if (seq.text.startsWith('✅')) {
setDemoStep(2);
}
}, seq.delay);
demoLogTimerRef.current.push(timer);
});
};
const submitDemoPrompt = (e: FormEvent) => {
e.preventDefault();
if (!demoPrompt.trim() || demoPending) return;
synth.playClick();
setDemoPending(true);
setDemoResponse("");
const mockAiResponses: { [key: string]: string } = {
default: "SYSTEM RESPONSE // SOVEREIGN ENGINE:\n\nInput processed completely on-device in 143ms.\nResult: Secure compliance extraction verified. Ready for download. Zero bits sent beyond localhost.",
analysts: "LOCAL_ANALYSIS //\nWe ran sentiment scoring on the structured CSV feed. Standard Deviances: \n- Positivity: 84.1%\n- Risk Alert: Trace detected on Cloud API usage. Highly recommend immediate on-site fallback protocol.",
extract: "EXTRACTED SCHEMA:\n{\n 'transaction_id': 'TX90812',\n 'sovereign_rating': '10.0',\n 'cost_per_token': '$0.000000000'\n}"
};
setTimeout(() => {
const p = demoPrompt.toLowerCase();
let response = mockAiResponses.default;
if (p.includes('analyst') || p.includes('score') || p.includes('sentiment')) {
response = mockAiResponses.analysts;
} else if (p.includes('schema') || p.includes('json') || p.includes('extract')) {
response = mockAiResponses.extract;
}
setDemoResponse(response);
setDemoPending(false);
synth.playBlip();
}, 1100);
};
const handleReconnect = () => {
synth.playClick();
setIsDisconnected(false);
setDemoStep(0);
};
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center max-w-6xl mx-auto w-full">
<div className="lg:col-span-4 flex flex-col justify-center text-left">
<span className="text-emerald-500 font-mono text-xs font-bold uppercase tracking-wider mb-2">// SOVEREIGN DISCONNECT</span>
<h2 className="font-display font-bold text-2xl sm:text-3xl text-white tracking-tight uppercase leading-none mb-4">
Live Offline Walkthrough
</h2>
<p className="text-slate-400 font-sans text-sm sm:text-base leading-relaxed mb-6">
Witness absolute independence in motion. Flip the sovereign kill-switch to isolate your computing workspace and verify local cognitive offline processing in high-fidelity.
</p>
<div className="hidden lg:flex items-center gap-2 border border-slate-800/80 rounded px-3 py-1.5 bg-slate-900/50 text-[10px] font-mono text-slate-500">
<Lock className="w-3.5 h-3.5 text-emerald-400 shrink-0 animate-pulse" />
<span>Isolating terminal loops from network cards fully.</span>
</div>
</div>
<div className="lg:col-span-8 flex flex-col w-full">
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-5 sm:p-6 text-left flex flex-col gap-4">
{!isDisconnected ? (
<div className="flex flex-col items-center justify-center py-10 text-center">
<WifiOff className="w-16 h-16 text-emerald-500/30 animate-pulse mb-6" />
<h3 className="font-display font-bold text-lg text-white uppercase mb-2">OFF-GRID CONNECTION SANDBOX</h3>
<p className="text-slate-400 text-xs font-sans max-w-md mb-8">
Initiate off-grid mode to sever connection mapping hooks and activate offline model simulations on your local VRAM nodes.
</p>
<button
onClick={handleSeverConnection}
className="px-6 py-3 rounded bg-emerald-500 text-slate-950 font-mono text-xs font-black uppercase tracking-wider border border-emerald-400 hover:shadow-[0_0_20px_rgba(16,185,129,0.35)] cursor-pointer transition-all duration-300"
>
ACTIVATE OFF-GRID INTERRUPT
</button>
</div>
) : (
<div className="flex flex-col gap-4 h-full">
<div className="flex items-center justify-between border-b border-slate-800 pb-2">
<div className="flex items-center gap-2 text-[10px] font-mono font-bold text-emerald-400">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse-emerald" />
<span>OFF-GRID SANDBOX ENVIRONMENT ACTIVE</span>
</div>
<button
onClick={handleReconnect}
className="font-mono text-[9px] text-slate-500 hover:text-slate-350 hover:underline"
>
[RECONNECT NETWORK]
</button>
</div>
<div className="bg-slate-900 rounded p-3 font-mono text-[10px] text-slate-300 h-32 overflow-y-auto custom-scrollbar flex flex-col gap-1.5 border border-slate-850">
{demoLogs.map((log, idx) => (
<p key={idx} className={log.includes('Failed') || log.includes('Dead') ? 'text-rose-450 font-bold' : log.includes('Sovereign') ? 'text-emerald-400 font-bold' : ''}>
{log}
</p>
))}
{demoStep === 1 && (
<div className="flex items-center gap-2 text-slate-500">
<span className="w-1.5 h-1.5 rounded-full bg-slate-500 animate-ping" />
<span>Severing network nodes...</span>
</div>
)}
</div>
{demoStep === 2 && (
<motion.form
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
onSubmit={submitDemoPrompt}
className="flex flex-col gap-3 font-mono"
>
<div className="flex flex-col gap-1">
<label className="text-[9px] text-slate-500 uppercase">Test Sovereign Reasoning Invocations:</label>
<div className="flex gap-2">
<input
type="text"
value={demoPrompt}
onChange={(e) => setDemoPrompt(e.target.value)}
placeholder="Try prompt e.g.: 'Run Sentiment Score Matrix' or 'Extract JSON Schema'..."
className="flex-1 bg-slate-900 border border-slate-800 rounded px-3 py-2 text-xs text-white focus:outline-none focus:border-emerald-500"
/>
<button
type="submit"
disabled={demoPending || !demoPrompt.trim()}
className="px-4 py-2 bg-emerald-500 text-slate-950 rounded font-black text-xs uppercase cursor-pointer hover:bg-emerald-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{demoPending ? 'RUNNING...' : 'SUBMIT'}
</button>
</div>
</div>
{(demoPending || demoResponse) && (
<div className="bg-slate-950 rounded border border-slate-850 p-3 h-28 overflow-y-auto custom-scrollbar font-mono text-[10px]">
{demoPending ? (
<div className="flex items-center gap-2 text-slate-500">
<div className="w-3 h-3 border border-t-emerald-500 border-r-transparent rounded-full animate-spin" />
<span>Invoking local model weights... SEC Compliance sandbox isolation 100% verified</span>
</div>
) : (
<pre className="text-emerald-400 break-words whitespace-pre-wrap">{demoResponse}</pre>
)}
</div>
)}
</motion.form>
)}
</div>
)}
</div>
</div>
</div>
);
}
+74
View File
@@ -0,0 +1,74 @@
import { Slide } from '../types';
export const SLIDES: Slide[] = [
{
id: 1,
title: "The Sovereign Business Engine",
subtitle: "Unlock Secure, Zero-Cost Local AI for Algarve Entrepreneurs, Nomads & Content Creators",
category: "INTRODUCTION"
},
{
id: 2,
title: "The Cloud Expense Trap",
points: [
"Data Leak Risks (Training on your client IP)",
"Unfair Subscriptions (A tax on every seat)",
"Sketchy Beach & Cafe Wi-Fi Downtime"
],
category: "RISK ANALYSIS"
},
{
id: 3,
title: "Your Private AI Workspace",
points: [
"Open-Source Brains (Free from big-tech license keys)",
"Visual Desktop Apps (Simple point-and-click layout)",
"100% Secure Offline Running (Zero data leaves your laptop)"
],
category: "ARCHITECTURE"
},
{
id: 4,
title: "AI Compression: Smart Scaling",
content: "Analogy: Premium high-end photo vs. optimized web graphic. Compression slashes memory needs by 75% so you can run powerful models directly on a normal laptop with no loss in logic.",
category: "PERFORMANCE OPTIMIZATION"
},
{
id: 5,
title: "Manual Overhead vs Smart Workflows",
points: [
"Stop playing chat ping-pong with slow prompts",
"Organize website posts, formatting, & emails instantly",
"Direct Clipboard-to-Excel / Content Management integrations"
],
category: "WORKFLOW AUTOMATION"
},
{
id: 6,
title: "Your Hardware Match",
points: [
"Standard Laptops: 16GB RAM (Excellent baseline)",
"Sweet Spot: 32GB-48GB RAM (Creative powerhouses)",
"Ultimate Studio: 64GB+ RAM (Complete cloud replacement)"
],
category: "HARDWARE MATRICES"
},
{
id: 7,
title: "Easy Setup & Hardware Guide",
subtitle: "Exactly what to buy or repurpose to replace expensive monthly software bills",
category: "BUYER'S GUIDE"
},
{
id: 8,
title: "Zero-Cost Lifetime ROI",
content: "Repurpose machines you already own, cut recurring monthly subscription spikes, and scale output to infinity for zero extra cost.",
category: "FINANCIAL FREEDOM"
},
{
id: 9,
title: "Live Offline Walkthrough",
content: "Pulling the internet plug. Absolute independence in action right here, right now.",
category: "LIVE DEMONSTRATION"
}
];
+72
View File
@@ -0,0 +1,72 @@
@import "tailwindcss";
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&family=JetBrains+Mono:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
@theme {
--font-sans: "Inter", sans-serif;
--font-mono: "JetBrains Mono", monospace;
--font-display: "Space Grotesk", sans-serif;
}
@layer utilities {
.cyber-grid {
background-size: 40px 40px;
background-image:
linear-gradient(to right, rgba(16, 185, 129, 0.04) 1px, transparent 1px),
linear-gradient(to bottom, rgba(16, 185, 129, 0.04) 1px, transparent 1px);
}
.cyber-glow {
box-shadow: 0 0 15px rgba(16, 185, 129, 0.15);
}
.cyber-glow-strong {
box-shadow: 0 0 25px rgba(16, 185, 129, 0.35);
}
.text-glow {
text-shadow: 0 0 8px rgba(16, 185, 129, 0.5);
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(16, 185, 129, 0.2);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(16, 185, 129, 0.4);
}
}
/* Scanline and flicker animations */
@keyframes cyber-scanline {
0% { transform: translateY(-100%); }
100% { transform: translateY(100%); }
}
.scanline::after {
content: " ";
display: block;
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
z-index: 2;
background-size: 100% 2px, 3px 100%;
pointer-events: none;
}
@keyframes pulse-emerald {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
.animate-pulse-emerald {
animation: pulse-emerald 2s infinite;
}
+10
View File
@@ -0,0 +1,10 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
+40
View File
@@ -0,0 +1,40 @@
// ==========================================
// TYPE DEFINITIONS
// ==========================================
export interface Slide {
id: number;
title: string;
subtitle?: string;
points?: string[];
content?: string;
category: string;
}
export interface ProcurementSpec {
machineType: string;
ramType: string;
bandWidth: string;
modelGoal: string;
verdict: string;
setupTools: string;
purchaseTip: string;
}
export type BudgetType = 'existing' | 'budget' | 'pro' | 'enterprise';
export type GoalType = 'writing' | 'rag' | 'agents';
export type QuantLevel = 'FP16' | 'Q8_0' | 'Q4_K_M' | 'Q2_K';
export type Slide6Tab = 'hardware' | 'tasks';
export interface ParsedData {
id: string;
name: string;
age: string;
region: string;
status: string;
}
export interface LogSequence {
text: string;
delay: number;
}
@@ -0,0 +1,145 @@
// ==========================================
// AUDIO SYNTHESIZER (Web Audio API)
// ==========================================
class CyberSynth {
private ctx: AudioContext | null = null;
public enabled: boolean = false;
private init() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
}
}
public playClick() {
if (!this.enabled) return;
try {
this.init();
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(880, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(120, this.ctx.currentTime + 0.08);
gain.gain.setValueAtTime(0.08, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.08);
osc.start();
osc.stop(this.ctx.currentTime + 0.08);
} catch (e) {
console.warn("Synth audio context failed: ", e);
}
}
public playBlip() {
if (!this.enabled) return;
try {
this.init();
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.type = 'triangle';
osc.frequency.setValueAtTime(1400, this.ctx.currentTime);
osc.frequency.setValueAtTime(1800, this.ctx.currentTime + 0.03);
gain.gain.setValueAtTime(0.04, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.06);
osc.start();
osc.stop(this.ctx.currentTime + 0.06);
} catch (e) {
// Ignored
}
}
public playError() {
if (!this.enabled) return;
try {
this.init();
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(120, this.ctx.currentTime);
osc.frequency.linearRampToValueAtTime(80, this.ctx.currentTime + 0.15);
gain.gain.setValueAtTime(0.1, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.15);
osc.start();
osc.stop(this.ctx.currentTime + 0.15);
} catch (e) {
// Ignored
}
}
public playSlideChange() {
if (!this.enabled) return;
try {
this.init();
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(440, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(660, this.ctx.currentTime + 0.12);
gain.gain.setValueAtTime(0.06, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.12);
osc.start();
osc.stop(this.ctx.currentTime + 0.12);
} catch (e) {
// Ignored
}
}
public playDisconnect() {
if (!this.enabled) return;
try {
this.init();
if (!this.ctx) return;
const osc1 = this.ctx.createOscillator();
const osc2 = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc1.connect(gain);
osc2.connect(gain);
gain.connect(this.ctx.destination);
osc1.type = 'triangle';
osc1.frequency.setValueAtTime(300, this.ctx.currentTime);
osc1.frequency.linearRampToValueAtTime(30, this.ctx.currentTime + 0.8);
osc2.type = 'sawtooth';
osc2.frequency.setValueAtTime(295, this.ctx.currentTime);
osc2.frequency.linearRampToValueAtTime(25, this.ctx.currentTime + 0.8);
gain.gain.setValueAtTime(0.12, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(0.001, this.ctx.currentTime + 0.8);
osc1.start();
osc2.start();
osc1.stop(this.ctx.currentTime + 0.8);
osc2.stop(this.ctx.currentTime + 0.8);
} catch (e) {
// Ignored
}
}
}
export const synth = new CyberSynth();
@@ -0,0 +1,114 @@
// ==========================================
// HARDWARE RECOMMENDATION ENGINE DATA
// ==========================================
import { ProcurementSpec, BudgetType, GoalType } from '../types';
export const getHardwareProcureData = (budget: BudgetType, goal: GoalType): ProcurementSpec => {
if (budget === 'existing') {
if (goal === 'writing') {
return {
machineType: "Standard Office Laptop (Apple MacBook Air or standard business PC)",
ramType: "8GB - 16GB RAM (Standard System RAM Share)",
bandWidth: "80 GB/s to 150 GB/s (System bottleneck)",
modelGoal: "Llama-3-8B-Instruct (Q4) or lightweight content models",
verdict: "FEASIBLE AT ZERO COST: Run right now! Use your regular machine. You'll get 15-20 words/sec. Perfect for drafting emails, creating social posts, or polishing website copies on a sunny terrace in Faro.",
setupTools: "LM Studio Desktop App (Simple offline chat app) or Jan Desktop",
purchaseTip: "Tip: Shut down heavy browser tabs or video call software to free up system memory before starting your local writing session."
};
} else if (goal === 'rag') {
return {
machineType: "High-end corporate laptop or office PC with 16GB-32GB RAM",
ramType: "16GB - 32GB RAM (DDR4/DDR5 system memory)",
bandWidth: "125 GB/s standard dual-channel memory",
modelGoal: "Qwen-2.5-7B or Mistral-7B-Instruct (Q4 quantized weights)",
verdict: "PRACTICAL BUT CAUTIOUS: Local secure search index via AnythingLLM works beautifully, but parsing long folders of business PDFs will take 2-4 seconds per page on a standard processor.",
setupTools: "AnythingLLM Desktop (runs secure built-in local vector storage on your system disk with zero code)",
purchaseTip: "Tip: Import files in modest batches rather than drag-and-dropping 100 giant PDFs simultaneously to prevent standard system lag."
};
} else {
return {
machineType: "Premium creator or executive device (minimum 32GB memory)",
ramType: "32GB RAM dual-channel speed configuration",
bandWidth: "150 GB/s to 200 GB/s standard speed",
modelGoal: "Llama-3-8B-Instruct (Q4) or Qwen-2.5-14B (Q4 Quantized)",
verdict: "LIMITED CPU SPEED: Multi-agent automation loops run slow on standard processors. A complex, multi-step workflow could take 10-15 seconds to execute complete sequences.",
setupTools: "AnythingLLM Multi-Agent workspace (drag-and-drop automated flows, no code required)",
purchaseTip: "Tip: For multi-step tasks, select ultra-compact models like Llama-3-3B to preserve instant typing response."
};
}
} else if (budget === 'budget') {
if (goal === 'writing') {
return {
machineType: "Mid-Range Creator Laptop or Custom Workstation with Nvidia Graphics Card",
ramType: "32GB System RAM + 12GB/16GB VRAM on graphics board",
bandWidth: ">300 GB/s VRAM speed on graphics memory",
modelGoal: "Llama-3-8B (uncompressed) or Qwen-2.5-14B (high-fidelity)",
verdict: "EXTREME SPEED: You will generate 50-70 words per second. Faster than standard cloud APIs. Unlimited writing or translations with zero delay while enjoying Albufeira beach views.",
setupTools: "LM Studio / Jan desktop app (maps directly to your graphics hardware automatically)",
purchaseTip: "Tip: Look for high-performance laptops with an Nvidia Geforce RTX 4060 or 4070 (minimum 12GB memory) for peak content writing speed."
};
} else if (goal === 'rag') {
return {
machineType: "Custom Business Desktop PC with Nvidia GeForce RTX 4070 Ti Super",
ramType: "32GB DDR5 + 16GB Dedicated Graphics VRAM",
bandWidth: "504 GB/s dedicated graphics bus bandwidth",
modelGoal: "Command-R 35B (Q3 compressed) or Llama-3.1-8B-Instruct (high-fidelity)",
verdict: "HIGH CAPACITY: Powerful 35B parameter models run seamlessly on-device. The 16GB graphics card holds entire business directories or marketing templates for instant secure lookup.",
setupTools: "AnythingLLM Desktop mapped to Ollama engine backing graphics acceleration (Zero Code!)",
purchaseTip: "Tip: Choose the 'Ti Super' variant specifically, as standard cards carry smaller VRAM buffers (12GB) which limit local model sizes."
};
} else {
return {
machineType: "Nvidia RTX 4080 (16GB VRAM) or RTX 4070 Super Workstation",
ramType: "64GB DDR5 System RAM + 16GB high-bandwidth graphics VRAM",
bandWidth: "716 GB/s extreme graphics memory speed",
modelGoal: "Qwen-2.5-14B-Instruct or Llama-3-8B-Instruct (runs fully offloaded to graphics memory)",
verdict: "ENTREPRENEUR SWEET SPOT: Outstanding automation speeds. Runs local action-assistants rapidly, turning multi-step website work or translation reviews into split-second tasks.",
setupTools: "AnythingLLM Desktop with Multi-Agent features (100% visual point-and-click setup)",
purchaseTip: "Tip: Look for desktop towers with good ventilation. Graphics cards will run at full capacity during intense marketing or content automation runs."
};
}
} else if (budget === 'pro') {
if (goal === 'writing') {
return {
machineType: "Apple Mac Studio (M3 Max, 128GB Unified Memory)",
ramType: "128GB Unified ultra-speed System Memory",
bandWidth: "400 GB/s high bandwidth unified architecture",
modelGoal: "Llama-3-70B-Instruct (Q4) or Qwen-2.5-32B (high-fidelity)",
verdict: "TOP COGNITIVE RATING: Write complex content reports, marketing plans, and legal briefs using state-of-the-art 70B parameter models at ~25 words/second with ultimate security.",
setupTools: "Open WebUI Desktop interface locally running on your Mac without code",
purchaseTip: "Tip: Mac Studio unified memory is shared between active screen displays and model layers. Leave 16GB margin for OS overhead."
};
} else if (goal === 'rag') {
return {
machineType: "Apple Mac Studio (128GB Memory) or Pro Workstation with RTX 4090 (24GB)",
ramType: "128GB Unified Memory or 24GB Ultra GDDR6X graphics memory",
bandWidth: "400 GB/s to 1,008 GB/s ultra-level graphics speed",
modelGoal: "Command R+ 104B (Q3) or Llama-3-70B-Instruct and local database in parallel",
verdict: "DEEP THINKER MATRIX: Indexes huge business catalogs, website records, and contracts (thousands of pages) in minutes. Query archives completely offline.",
setupTools: "AnythingLLM visual workspace server running completely locally on your system desk",
purchaseTip: "Tip: Speed is determined by memory bandwidth. Focus entirely on graphics bandwidth (over 500GB/s is the sweet spot!)."
};
} else {
return {
machineType: "Pro Workstation with 2x Nvidia RTX 4090 or Mac Studio M3 Ultra (192GB RAM)",
ramType: "48GB Graphics Memory (Dual Cards) or 192GB Unified Memory",
bandWidth: "Over 800 GB/s massive pipeline speed",
modelGoal: "Mixtral 8x22B (Q4) or customized local 70B workflow automation pipelines",
verdict: "CREATIVE AGENCY POWER: Ultimate speed for simultaneous content generation and data tasks. Automatically drafts, translates, and formats blog posts in parallel.",
setupTools: "AnythingLLM visual agency tools or local docker visual desktop apps",
purchaseTip: "Tip: Make sure you use a top-shelf power supply if custom-building dual graphics cards to handle high content workloads gracefully."
};
}
} else {
return {
machineType: "Dedicated Private In-House Server Nodes (Gigabyte Server with 4x Nvidia RTX cards)",
ramType: "96GB to 384GB high-grade Server Memory + massive graphics pools",
bandWidth: "1.5 TB/s to 3.2 TB/s ultra-bandwidth networks",
modelGoal: "Llama-3.1-405B (Q4 quantization) or multiple parallel Mixtral instances",
verdict: "COMPLETE OFFICE CLOUD REPLACEMENT: Serves private email, content, and search tools across your entire company. Replaces team subscriptions completely.",
setupTools: "LM Studio Commercial Server or AnythingLLM Enterprise Workspace (Simple admin dashboard)",
purchaseTip: "Tip: Place these in a cooled server room or closet. Enterprise server configurations require proper cooling and ventilation."
};
}
};
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}
+15
View File
@@ -0,0 +1,15 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import {defineConfig} from 'vite';
export default defineConfig(() => {
return {
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
}
};
});
@@ -0,0 +1,33 @@
Here are the extracted action items, tasks, and commitments:
1. **Who**: Unassigned
**What**: Review and revise meeting notes to ensure accuracy and completeness.
**When**: No deadline specified
**Priority**: Low
2. **Who**: Alaz
**What**: Follow up with Snorri regarding his dimensional teapot and its contents.
**When**: No deadline specified
**Priority**: Medium
3. **Who**: Tuxi
**What**: Provide Alaz with more information about the dimensional teapot's prize.
**When**: No deadline specified
**Priority**: Low
4. **Who**: Unassigned
**What**: Research and provide answers to Snorri's questions regarding the family portraits in his lounge.
**When**: No deadline specified
**Priority**: Medium
5. **Who**: Alaz
**What**: Investigate the origins of the jar image found in Snorri's lounge.
**When**: No deadline specified
**Priority**: Low
6. **Who**: Unassigned
**What**: Clarify and resolve any outstanding questions or issues from the meeting.
**When**: No deadline specified
**Priority**: High
Note: The conversation at the end of the transcript appears to be a discussion about whether anything was missed, but no specific action items were assigned.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,20 @@
**Topic**: **Campaign Session Recap**
**Key Points**:
* The group participated in a combat encounter with the Beatles, using creative methods to defeat them.
* A "beat-o" was defeated by Snorri using an apple, granting him a ghostly speed ability for a week.
* The group prepared and presented three dishes: Tuxi's colorful boom-boom decoration, Snorri's sashimi, and Alaz's flamed dish.
* For dessert, the group went on a foraging task, with Tuxy preparing lychee surprise and Snorri making a lingon berry jelly.
* Alaz visited a village of awakened apes and obtained ingredients to make a rice pudding.
**Decisions**:
* The competition was decided, with Alaz winning a dimensional teapot and Snorri coming second with an arcane Rubik's gift.
* Snorri exchanged prizes with Alaz, obtaining the teapot and rubbing it three times to teleport into a different dimension.
* Snorri obtained an image of a jar from the new dimension.
**Open Questions**:
* What is the significance of the family portraits in the new dimension?
* How will the group's experience in this new dimension affect their future gameplay?
File diff suppressed because one or more lines are too long
+178
View File
@@ -0,0 +1,178 @@
# 🎤 meetings — local audio → transcript → summary + action items
A single-file shell script CLI that transcribes meeting recordings (using GGUF Whisper or Parakeet models), then generates a summary and extracts action items using Ollama. Everything runs 100% locally.
```
audio file ──▶ ffmpeg ──▶ whisper.cpp / parakeet.cpp ──▶ Ollama ──▶ report.md
│ │ │
16kHz mono WAV GGUF transcription summary + actions
```
## Quick start
```bash
# 1. Clone/download
git clone <this-repo> meetings-cli && cd meetings-cli
# 2. One-time setup (installs whisper.cpp, downloads model, pulls Ollama LLM)
./meetings setup
# 3. Process a meeting recording
./meetings recording.mp3
# 4. Check everything is healthy
./meetings doctor
```
## What it does
| Step | Tool | What happens |
|------|------|--------------|
| 1. Convert | ffmpeg | Any audio → 16kHz mono WAV |
| 2. Transcribe | whisper.cpp or parakeet.cpp | GGUF/GGML model → text transcript |
| 3. Summarize | Ollama | Transcript → structured summary (topic, key points, decisions, open questions) |
| 4. Extract | Ollama | Transcript → numbered action items (who, what, when, priority) |
## Output
For each audio file, a directory is created containing:
```
2026-06-07_1402_team_standup/
├── report.md # Combined: summary + actions + transcript
├── transcript.txt # Raw transcription
├── summary.md # LLM-generated summary
└── action_items.md # Extracted action items
```
## Requirements
| Dependency | Install | Purpose |
|------------|---------|---------|
| **ffmpeg** | `brew install ffmpeg` | Audio format conversion |
| **whisper.cpp** | `brew install whisper-cpp` | Speech-to-text (GGML models) |
| **Ollama** | [ollama.com](https://ollama.com) | LLM for summarization |
| **jq** | `brew install jq` | JSON parsing for Ollama API |
> `./meetings setup` handles all of this automatically.
## STT engines
### whisper.cpp (default, recommended)
- Battle-tested, many languages, large model ecosystem
- Models from [ggerganov/whisper.cpp](https://huggingface.co/ggerganov/whisper.cpp)
- Install: `brew install whisper-cpp`
| Model | Size | Best for |
|-------|------|----------|
| tiny.en | 75 MB | Quick tests, English |
| base.en | 142 MB | Good balance, English |
| small.en | 466 MB | **Recommended for English** |
| medium.en | 1.5 GB | High accuracy, English |
| large-v3-turbo | 809 MB | Best multilingual, fast |
| large-v3 | 2.9 GB | Best accuracy, any language |
### parakeet.cpp (alternative, faster)
- NVIDIA Parakeet models, excellent English, smaller footprint
- Models from [mudler/parakeet-cpp-gguf](https://huggingface.co/mudler/parakeet-cpp-gguf)
- Install: Build from [source](https://github.com/mudler/parakeet.cpp) or use Docker
| Model | Size | Best for |
|-------|------|----------|
| tdt_ctc-110m-q8_0 | 178 MB | Fast, good English |
| tdt_ctc-110m-f16 | 268 MB | Fast, lossless English |
| tdt-0.6b-v3-f16 | 1.4 GB | Multilingual |
## Configuration
### Environment variables
```bash
MEETINGS_DIR # Config & models directory (default: ~/.meetings)
MEETINGS_STT # STT engine: whisper | parakeet
MEETINGS_STT_MODEL # Path to GGUF/GGML model file
MEETINGS_LLM # Ollama model for summarization (default: llama3.1:8b)
MEETINGS_THREADS # Thread count for STT (default: 4)
MEETINGS_LANG # Language code (default: en; use "auto" for multilingual)
MEETINGS_OUTPUT # Output directory (default: .)
```
### CLI flags
```bash
./meetings recording.mp3 --stt whisper --llm llama3.1:8b --lang en --output ./reports
```
### Config file
Saved at `~/.meetings/config` after running `./meetings setup`:
```
STT_ENGINE=whisper
OLLAMA_MODEL=llama3.1:8b
THREADS=4
STT_MODEL=/home/user/.meetings/models/ggml-small.en.bin
```
## Commands
```bash
./meetings <audio_file> # Run the full pipeline
./meetings setup # Install deps + download model (interactive)
./meetings doctor # Check all dependencies
./meetings config # Show current configuration
./meetings help # Show help
```
## How Ollama fits in
**Ollama does NOT run the whisper/parakeet models** — those use their own inference engines (whisper.cpp / parakeet.cpp). Ollama is only used for the LLM steps:
1. **Summary generation** — sends the transcript to an Ollama model with a structured summarization prompt
2. **Action item extraction** — sends the transcript to an Ollama model with an action-item extraction prompt
You can use any Ollama model. Smaller models (llama3.2:1b, gemma3:1b) are faster; larger models (llama3.1:8b, qwen2.5-coder:7b) produce better summaries.
## Example
```bash
$ ./meetings team_standup.m4a
┌─────────────────────────────────────────────┐
│ 🎤 M E E T I N G S │
│ audio → transcript → summary + actions │
│ whisper.cpp · parakeet.cpp · ollama │
└─────────────────────────────────────────────┘
Input: team_standup.m4a
STT engine: whisper
STT model: ggml-small.en.bin
LLM model: llama3.1:8b
Language: en
Output: ./2026-06-07_1402_team_standup/
── Step 1/4 — Converting audio ──
▸ Converting audio to 16kHz mono WAV...
✓ Audio converted: 1.2M
── Step 2/4 — Transcribing with whisper ──
▸ Transcribing with whisper.cpp...
✓ Transcript: 847 words
── Step 3/4 — Summarizing (llama3.1:8b) ──
✓ Summary saved
── Step 4/4 — Extracting action items (llama3.1:8b) ──
✓ Action items saved
✓ All done! Files saved to: ./2026-06-07_1402_team_standup/
📄 Report: ./2026-06-07_1402_team_standup/report.md
📝 Transcript: ./2026-06-07_1402_team_standup/transcript.txt
📋 Summary: ./2026-06-07_1402_team_standup/summary.md
✅ Action Items: ./2026-06-07_1402_item_standup/action_items.md
```
## License
MIT
+659
View File
@@ -0,0 +1,659 @@
#!/usr/bin/env bash
#
# meetings — audio → transcript → summary + action items, all local
#
# Transcribes audio using whisper.cpp or parakeet.cpp (GGUF/GGML models),
# then uses Ollama to summarize and extract action items.
#
# Usage:
# ./meetings <audio_file> # full pipeline
# ./meetings setup # install deps + download model
# ./meetings doctor # check dependencies
# ./meetings config # show current config
#
set -euo pipefail
# ─── colours ────────────────────────────────────────────────────────
RED='\033[0;31m'; GRN='\033[0;32m'; YEL='\033[1;33m'
BLU='\033[0;34m'; CYN='\033[0;36m'; RST='\033[0m'
BOLD='\033[1m'
# ─── paths ──────────────────────────────────────────────────────────
MEETINGS_DIR="${MEETINGS_DIR:-$HOME/.meetings}"
CONFIG_FILE="$MEETINGS_DIR/config"
MODELS_DIR="$MEETINGS_DIR/models"
# ─── helpers ───────────────────────────────────────────────────────
die() { printf "${RED}error: %s${RST}\n" "$*" >&2; exit 1; }
info() { printf "${BLU}▸ %s${RST}\n" "$*" >&2; }
ok() { printf "${GRN}✓ %s${RST}\n" "$*" >&2; }
warn() { printf "${YEL}⚠ %s${RST}\n" "$*" >&2; }
step() { printf "\n${BOLD}${CYN}── %s ──${RST}\n" "$*" >&2; }
banner() {
cat <<'BAN'
┌─────────────────────────────────────────────┐
│ 🎤 M E E T I N G S │
│ audio → transcript → summary + actions │
│ whisper.cpp · parakeet.cpp · ollama │
└─────────────────────────────────────────────┘
BAN
}
# ─── load config: env vars > config file > defaults ────────────────
load_config() {
# defaults
STT_ENGINE=""
STT_MODEL=""
OLLAMA_MODEL="llama3.1:8b"
OLLAMA_HOST="http://localhost:11434"
THREADS="4"
LANGUAGE="en"
OUTPUT_DIR="."
# config file overrides defaults
[[ -f "$CONFIG_FILE" ]] && source "$CONFIG_FILE"
# env vars override everything (|| true prevents set -e exit on empty vars)
[[ -n "${MEETINGS_STT:-}" ]] && STT_ENGINE="$MEETINGS_STT" || true
[[ -n "${MEETINGS_STT_MODEL:-}" ]] && STT_MODEL="$MEETINGS_STT_MODEL" || true
[[ -n "${MEETINGS_LLM:-}" ]] && OLLAMA_MODEL="$MEETINGS_LLM" || true
[[ -n "${MEETINGS_THREADS:-}" ]] && THREADS="$MEETINGS_THREADS" || true
[[ -n "${MEETINGS_LANG:-}" ]] && LANGUAGE="$MEETINGS_LANG" || true
[[ -n "${MEETINGS_OUTPUT:-}" ]] && OUTPUT_DIR="$MEETINGS_OUTPUT" || true
}
# ─── detect STT engine ──────────────────────────────────────────────
detect_stt() {
if command -v parakeet-cli &>/dev/null; then
echo "parakeet"
elif command -v whisper-cli &>/dev/null; then
echo "whisper"
else
echo ""
fi
}
get_stt_binary() {
local engine="${1:-$STT_ENGINE}"
case "$engine" in
whisper) command -v whisper-cli 2>/dev/null || echo "" ;;
parakeet) command -v parakeet-cli 2>/dev/null || echo "" ;;
*) echo "" ;;
esac
}
find_stt_model() {
local engine="${1:-whisper}"
if [[ -n "${STT_MODEL:-}" && -f "${STT_MODEL}" ]]; then
echo "$STT_MODEL"; return 0
fi
case "$engine" in
whisper)
for f in "$MODELS_DIR"/ggml-*.bin "$MODELS_DIR"/ggml-*.gguf; do
[[ -f "$f" ]] && echo "$f" && return 0
done
;;
parakeet)
for f in "$MODELS_DIR"/*parakeet*.gguf "$MODELS_DIR"/*tdt*.gguf; do
[[ -f "$f" ]] && echo "$f" && return 0
done
;;
esac
return 1
}
# ─── doctor ─────────────────────────────────────────────────────────
cmd_doctor() {
banner >&2
local ok_count=0 total=0
total=$((total+1))
if command -v ffmpeg &>/dev/null; then
ok "ffmpeg: $(command -v ffmpeg)"; ok_count=$((ok_count+1))
else
warn "ffmpeg: not found"
fi
total=$((total+1))
if command -v ollama &>/dev/null; then
ok "ollama: $(command -v ollama)"; ok_count=$((ok_count+1))
if curl -sf "${OLLAMA_HOST:-http://localhost:11434}/api/tags" &>/dev/null; then
ok " server: running at ${OLLAMA_HOST:-http://localhost:11434}"
else
warn " server: not responding (run: ollama serve)"
fi
else
warn "ollama: not found"
fi
total=$((total+1))
local engine="${STT_ENGINE:-$(detect_stt)}"
local bin
bin="$(get_stt_binary "$engine")"
if [[ -n "$bin" ]]; then
ok "STT engine: $engine ($bin)"; ok_count=$((ok_count+1))
else
warn "STT engine: not found (run: ./meetings setup)"
fi
total=$((total+1))
local model
model="$(find_stt_model "$engine")" || true
if [[ -n "$model" ]]; then
ok "STT model: $model"; ok_count=$((ok_count+1))
else
warn "STT model: not found (run: ./meetings setup)"
fi
total=$((total+1))
local ollama_model="${OLLAMA_MODEL:-llama3.1:8b}"
local model_name
model_name="$(echo "$ollama_model" | cut -d: -f1)"
if ollama list 2>/dev/null | awk '{print $1}' | grep -qF "$model_name"; then
ok "LLM model: $ollama_model (pulled)"; ok_count=$((ok_count+1))
else
warn "LLM model: $ollama_model (not pulled — run: ollama pull $ollama_model)"
fi
echo "" >&2
if [[ $ok_count -eq $total ]]; then
ok "All good ($ok_count/$total)"
else
warn "Ready ($ok_count/$total). Run './meetings setup' to install missing pieces."
fi
}
# ─── setup ──────────────────────────────────────────────────────────
cmd_setup() {
load_config
banner >&2
mkdir -p "$MEETINGS_DIR" "$MODELS_DIR"
# ── ffmpeg ──
step "Checking ffmpeg"
if command -v ffmpeg &>/dev/null; then
ok "ffmpeg already installed"
else
info "Installing ffmpeg..."
if [[ "$(uname)" == "Darwin" ]]; then
brew install ffmpeg || die "Could not install ffmpeg"
elif command -v apt-get &>/dev/null; then
sudo apt-get update && sudo apt-get install -y ffmpeg
elif command -v dnf &>/dev/null; then
sudo dnf install -y ffmpeg
else
die "Please install ffmpeg manually: https://ffmpeg.org/download.html"
fi
fi
# ── ollama ──
step "Checking Ollama"
if command -v ollama &>/dev/null; then
ok "ollama already installed"
else
info "Installing Ollama..."
curl -fsSL https://ollama.com/install.sh | sh || die "Could not install Ollama"
fi
if ! curl -sf "${OLLAMA_HOST:-http://localhost:11434}/api/tags" &>/dev/null; then
info "Starting Ollama server..."
ollama serve &>/dev/null &
sleep 3
fi
# ── choose STT engine ──
step "Choosing STT engine"
echo "" >&2
echo " 1) whisper.cpp — battle-tested, many languages, brew install" >&2
echo " 2) parakeet.cpp — faster, great English, smaller footprint" >&2
echo "" >&2
local choice
if [[ -n "${STT_ENGINE:-}" ]]; then
choice="$STT_ENGINE"
info "Using pre-configured engine: $choice"
else
read -rp " Choose [1/2, default=1]: " choice
case "${choice:-1}" in
2|parakeet) choice="parakeet" ;;
*) choice="whisper" ;;
esac
fi
case "$choice" in
whisper) setup_whisper ;;
parakeet) setup_parakeet ;;
esac
STT_ENGINE="$choice"
grep -q "^STT_ENGINE=" "$CONFIG_FILE" 2>/dev/null \
&& sed -i '' "s/^STT_ENGINE=.*/STT_ENGINE=$choice/" "$CONFIG_FILE" 2>/dev/null \
|| echo "STT_ENGINE=$choice" >> "$CONFIG_FILE"
# ── pull LLM ──
step "Pulling LLM: ${OLLAMA_MODEL:-llama3.1:8b}"
local ollama_model="${OLLAMA_MODEL:-llama3.1:8b}"
if ollama list 2>/dev/null | awk '{print $1}' | grep -qF "$(echo "$ollama_model" | cut -d: -f1)"; then
ok "$ollama_model already pulled"
else
ollama pull "$ollama_model" || warn "Could not pull $ollama_model. Run: ollama pull $ollama_model"
fi
echo "" >&2
ok "Setup complete! Run './meetings doctor' to verify, then './meetings <audio_file>' to process."
}
setup_whisper() {
step "Installing whisper.cpp"
if command -v whisper-cli &>/dev/null; then
ok "whisper-cli already available at $(command -v whisper-cli)"
else
info "Installing via package manager..."
if [[ "$(uname)" == "Darwin" ]]; then
brew install whisper-cpp || die "Could not install whisper-cpp via brew"
else
die "Please install whisper-cli manually: https://github.com/ggml-org/whisper.cpp
On macOS: brew install whisper-cpp
Or build from source: git clone https://github.com/ggml-org/whisper.cpp && cd whisper.cpp && cmake -B build && cmake --build build -j"
fi
fi
step "Downloading Whisper model"
local model_choice
echo "" >&2
echo " Available models (from HuggingFace ggerganov/whisper.cpp):" >&2
echo " tiny.en — 75 MB (fastest, English only)" >&2
echo " base.en — 142 MB (good for quick tests)" >&2
echo " small.en — 466 MB (recommended for English) ★" >&2
echo " medium.en — 1.5 GB (high accuracy, English only)" >&2
echo " large-v3-turbo — 809 MB (best multilingual, fast)" >&2
echo " large-v3 — 2.9 GB (best accuracy, any language)" >&2
echo "" >&2
if [[ -n "${STT_MODEL:-}" && -f "${STT_MODEL}" ]]; then
info "Using existing model: $STT_MODEL"
else
read -rp " Choose model [default=small.en]: " model_choice
model_choice="${model_choice:-small.en}"
local model_file="$MODELS_DIR/ggml-${model_choice}.bin"
if [[ -f "$model_file" ]]; then
ok "Model already downloaded: $model_file"
else
info "Downloading ggml-${model_choice}.bin (this may take a moment)..."
curl -L --progress-bar \
-o "$model_file" \
"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-${model_choice}.bin" \
|| die "Failed to download model"
ok "Downloaded: $model_file"
fi
STT_MODEL="$model_file"
echo "STT_MODEL=$model_file" >> "$CONFIG_FILE"
fi
}
setup_parakeet() {
step "Installing parakeet.cpp"
if command -v parakeet-cli &>/dev/null; then
ok "parakeet-cli already available at $(command -v parakeet-cli)"
else
info "Checking for pre-built binary or Docker..."
if command -v docker &>/dev/null; then
ok "Docker available — parakeet.cpp will run via container"
else
die "parakeet-cli not found and Docker not installed. Please either:
- Build parakeet.cpp: https://github.com/mudler/parakeet.cpp
- Or install Docker for the container approach"
fi
fi
step "Downloading Parakeet model"
local model_choice
echo "" >&2
echo " Available models (from HuggingFace mudler/parakeet-cpp-gguf):" >&2
echo " tdt_ctc-110m-q8_0 — 178 MB (fast, good English, smallest) ★" >&2
echo " tdt_ctc-110m-f16 — 268 MB (fast, lossless English)" >&2
echo " tdt-0.6b-v3-f16 — 1.4 GB (multilingual, recommended)" >&2
echo "" >&2
if [[ -n "${STT_MODEL:-}" && -f "${STT_MODEL}" ]]; then
info "Using existing model: $STT_MODEL"
else
read -rp " Choose model [default=tdt_ctc-110m-q8_0]: " model_choice
model_choice="${model_choice:-tdt_ctc-110m-q8_0}"
local model_file="$MODELS_DIR/${model_choice}.gguf"
if [[ -f "$model_file" ]]; then
ok "Model already downloaded: $model_file"
else
info "Downloading ${model_choice}.gguf..."
curl -L --progress-bar \
-o "$model_file" \
"https://huggingface.co/mudler/parakeet-cpp-gguf/resolve/main/${model_choice}.gguf" \
|| die "Failed to download model"
ok "Downloaded: $model_file"
fi
STT_MODEL="$model_file"
echo "STT_MODEL=$model_file" >> "$CONFIG_FILE"
fi
}
# ─── config display ─────────────────────────────────────────────────
cmd_config() {
load_config
banner >&2
echo "" >&2
if [[ -f "$CONFIG_FILE" ]]; then
echo " Saved config ($CONFIG_FILE):" >&2
echo "" >&2
while IFS='=' read -r key val; do
printf " %-16s %s\n" "$key" "$val" >&2
done < "$CONFIG_FILE"
else
echo " (no saved config — run: ./meetings setup)" >&2
fi
echo "" >&2
echo " Effective settings:" >&2
local stt_display="${STT_ENGINE:-$(detect_stt)}"
stt_display="${stt_display:-(not set)}"
echo " STT engine: $stt_display" >&2
echo " STT model: ${STT_MODEL:-(auto-detect)}" >&2
echo " LLM model: $OLLAMA_MODEL" >&2
echo " Ollama host: $OLLAMA_HOST" >&2
echo " Threads: $THREADS" >&2
echo " Language: $LANGUAGE" >&2
echo " Output dir: $OUTPUT_DIR" >&2
echo "" >&2
}
# ─── convert audio ──────────────────────────────────────────────────
convert_audio() {
local input="$1" output="$2"
info "Converting audio to 16kHz mono WAV..."
ffmpeg -y -i "$input" -ar 16000 -ac 1 -c:a pcm_s16le "$output" \
-loglevel warning 2>/dev/null \
|| die "ffmpeg conversion failed for $input"
ok "Audio converted: $(du -h "$output" | cut -f1)"
}
# ─── transcribe with whisper ───────────────────────────────────────
transcribe_whisper() {
local wav_file="$1" model="$2" binary="$3"
info "Transcribing with whisper.cpp..."
local lang_flag=""
[[ "$LANGUAGE" != "auto" ]] && lang_flag="-l $LANGUAGE"
local transcript
transcript=$("$binary" \
-m "$model" \
-f "$wav_file" \
-t "$THREADS" \
$lang_flag \
-nt \
--no-prints \
2>/dev/null) || die "Whisper transcription failed"
echo "$transcript"
}
# ─── transcribe with parakeet ──────────────────────────────────────
transcribe_parakeet() {
local wav_file="$1" model="$2" binary="$3"
info "Transcribing with parakeet.cpp..."
local decoder_flag=""
if echo "$model" | grep -qi "ctc"; then
decoder_flag="--decoder ctc"
elif echo "$model" | grep -qi "tdt"; then
decoder_flag="--decoder tdt"
fi
if [[ -z "$binary" ]] && command -v docker &>/dev/null; then
info "Using Docker for parakeet.cpp..."
docker run --rm \
-v "$model:/model.gguf:ro" \
-v "$wav_file:/audio.wav:ro" \
ghcr.io/mudler/parakeet.cpp-cli:latest \
transcribe --model /model.gguf --input /audio.wav $decoder_flag 2>/dev/null \
|| die "Docker parakeet transcription failed"
else
"$binary" transcribe --model "$model" --input "$wav_file" $decoder_flag 2>/dev/null \
|| die "Parakeet transcription failed"
fi
}
# ─── call Ollama ───────────────────────────────────────────────────
ollama_generate() {
local prompt="$1" system="${2:-}"
local json_payload
if [[ -n "$system" ]]; then
json_payload=$(jq -n \
--arg model "$OLLAMA_MODEL" \
--arg system "$system" \
--arg prompt "$prompt" \
'{model: $model, system: $system, prompt: $prompt, stream: false}')
else
json_payload=$(jq -n \
--arg model "$OLLAMA_MODEL" \
--arg prompt "$prompt" \
'{model: $model, prompt: $prompt, stream: false}')
fi
curl -sf "$OLLAMA_HOST/api/generate" \
-d "$json_payload" 2>/dev/null \
| jq -r '.response // empty' \
|| die "Ollama request failed. Is the server running? (ollama serve)"
}
# ─── main pipeline ──────────────────────────────────────────────────
cmd_process() {
local input_file="$1"
shift || true
# Load config BEFORE flag parsing, so flags can override
load_config
while [[ $# -gt 0 ]]; do
case "$1" in
--stt) STT_ENGINE="$2"; shift 2 ;;
--model) STT_MODEL="$2"; shift 2 ;;
--llm) OLLAMA_MODEL="$2"; shift 2 ;;
--lang) LANGUAGE="$2"; shift 2 ;;
--threads) THREADS="$2"; shift 2 ;;
--output) OUTPUT_DIR="$2"; shift 2 ;;
*) die "Unknown option: $1. Run: ./meetings help" ;;
esac
done
[[ -z "$input_file" ]] && die "Usage: ./meetings <audio_file>"
[[ ! -f "$input_file" ]] && die "File not found: $input_file"
STT_ENGINE="${STT_ENGINE:-$(detect_stt)}"
[[ -z "$STT_ENGINE" ]] && die "No STT engine found. Run: ./meetings setup"
local binary
binary="$(get_stt_binary "$STT_ENGINE")"
[[ -z "$binary" ]] && die "Could not find $STT_ENGINE binary. Run: ./meetings setup"
local model
if [[ -n "${STT_MODEL:-}" ]] && [[ -f "$STT_MODEL" ]]; then
model="$STT_MODEL"
else
model="$(find_stt_model "$STT_ENGINE")" \
|| die "No STT model found. Run: ./meetings setup"
fi
[[ ! -f "$model" ]] && die "Model file not found: $model"
command -v ffmpeg &>/dev/null || die "ffmpeg not found. Install it first."
curl -sf "$OLLAMA_HOST/api/tags" &>/dev/null \
|| die "Ollama server not responding at $OLLAMA_HOST. Run: ollama serve"
# ── create output directory ──
local basename
basename="$(date +%Y-%m-%d_%H%M)_$(basename "${input_file%.*}" | tr ' ' '_')"
local outdir="$OUTPUT_DIR/$basename"
mkdir -p "$outdir"
banner >&2
echo " Input: $input_file" >&2
echo " STT engine: $STT_ENGINE" >&2
echo " STT model: $(basename "$model")" >&2
echo " LLM model: $OLLAMA_MODEL" >&2
echo " Language: $LANGUAGE" >&2
echo " Output: $outdir/" >&2
echo "" >&2
# ── step 1: convert ──
step "Step 1/4 — Converting audio"
local wav_file="$outdir/audio_16k.wav"
convert_audio "$input_file" "$wav_file"
# ── step 2: transcribe ──
step "Step 2/4 — Transcribing with $STT_ENGINE"
local transcript
case "$STT_ENGINE" in
whisper) transcript="$(transcribe_whisper "$wav_file" "$model" "$binary")" ;;
parakeet) transcript="$(transcribe_parakeet "$wav_file" "$model" "$binary")" ;;
*) die "Unknown engine: $STT_ENGINE" ;;
esac
if [[ -z "$transcript" || -z "$(echo "$transcript" | tr -d '[:space:]')" ]]; then
die "Transcription produced empty output"
fi
echo "$transcript" > "$outdir/transcript.txt"
local word_count
word_count=$(echo "$transcript" | wc -w | tr -d ' ')
ok "Transcript: $word_count words"
# ── step 3: summarize ──
step "Step 3/4 — Summarizing ($OLLAMA_MODEL)"
local summarize_prompt
summarize_prompt="Please provide a clear, concise summary of the following meeting transcript.
Structure the summary with:
- **Topic**: What the meeting was about
- **Key Points**: Main topics discussed (3-5 bullet points)
- **Decisions**: Any decisions that were made
- **Open Questions**: Things left unresolved
TRANSCRIPT:
$transcript"
local summary
summary="$(ollama_generate "$summarize_prompt" "You are an expert meeting summarizer. Be concise but thorough. Always use markdown formatting.")"
echo "$summary" > "$outdir/summary.md"
ok "Summary saved"
# ── step 4: action items ──
step "Step 4/4 — Extracting action items ($OLLAMA_MODEL)"
local actions_prompt
actions_prompt="Extract all action items, tasks, and commitments from the following meeting transcript.
For each action item, provide:
- **Who**: The person responsible (or 'Unassigned' if unclear)
- **What**: The specific task or action
- **When**: Any deadline mentioned (or 'No deadline specified')
- **Priority**: High / Medium / Low (based on urgency and context)
Be thorough — capture every commitment, follow-up, and task mentioned or implied.
Format as a numbered list.
TRANSCRIPT:
$transcript"
local actions
actions="$(ollama_generate "$actions_prompt" "You are an expert at extracting action items from meeting notes. Be thorough and specific. Always use markdown formatting.")"
echo "$actions" > "$outdir/action_items.md"
ok "Action items saved"
# ── combine report ──
cat > "$outdir/report.md" <<REPORT
# Meeting Report
**Date:** $(date '+%B %d, %Y at %H:%M')
**Source:** $(basename "$input_file")
**STT:** $STT_ENGINE | **LLM:** $OLLAMA_MODEL
---
## Summary
$summary
---
## Action Items
$actions
---
## Full Transcript
$transcript
REPORT
echo "" >&2
ok "All done! Files saved to: $outdir/" >&2
echo "" >&2
echo " 📄 Report: $outdir/report.md" >&2
echo " 📝 Transcript: $outdir/transcript.txt" >&2
echo " 📋 Summary: $outdir/summary.md" >&2
echo " ✅ Action Items: $outdir/action_items.md" >&2
echo "" >&2
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
echo "" >&2
head -50 "$outdir/report.md" | tail -35 >&2
echo "" >&2
echo "(showing preview — full report at $outdir/report.md)" >&2
}
# ─── entry point ────────────────────────────────────────────────────
case "${1:-help}" in
setup) shift; cmd_setup ;;
doctor) load_config; cmd_doctor ;;
config) cmd_config ;;
help|--help|-h)
banner >&2
cat <<'HELP'
Usage:
./meetings <audio_file> Run the full pipeline
./meetings setup Install deps + download model
./meetings doctor Check all dependencies
./meetings config Show current configuration
Options (override config):
--stt <whisper|parakeet> STT engine to use
--model <path> Path to GGUF/GGML model file
--llm <ollama_model> Ollama model for summarization
--lang <code> Language code (default: en, auto for whisper)
--threads <N> Number of threads for STT
--output <dir> Output directory (default: .)
Environment variables:
MEETINGS_DIR Config & models directory (default: ~/.meetings)
MEETINGS_STT STT engine (whisper/parakeet)
MEETINGS_STT_MODEL Path to model file
MEETINGS_LLM Ollama model name (default: llama3.1:8b)
MEETINGS_THREADS Thread count (default: 4)
MEETINGS_LANG Language code (default: en)
MEETINGS_OUTPUT Output directory (default: .)
Examples:
# First time — install everything
./meetings setup
# Process a meeting recording
./meetings recording.mp3
# Use a different LLM model
./meetings meeting.wav --llm llama3.1:8b
# Use parakeet with multilingual model
./meetings call.wav --stt parakeet --lang auto
# Specify output directory
./meetings interview.m4a --output ./reports
HELP
;;
*)
cmd_process "$@"
;;
esac