adding the first version of the presentation and the initial attempt at the .meetings script
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
*.mp3
|
||||||
|
*.bin
|
||||||
|
.DS_Store
|
||||||
|
*.wav
|
||||||
@@ -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
|
||||||
@@ -1 +1,8 @@
|
|||||||
## The local AI demo for LEDEX Events
|
## 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
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
## Local AI presentation for LEDEX Portimao 2026
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "Local AI Presentation",
|
||||||
|
"description": "A responsive Sovereign/Cyber presentation carousel on Local AI for Business.",
|
||||||
|
"requestFramePermissions": []
|
||||||
|
}
|
||||||
Generated
+3964
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 > 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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>,
|
||||||
|
);
|
||||||
@@ -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."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
Executable
+659
@@ -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
|
||||||
Reference in New Issue
Block a user