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