Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 149 additions & 30 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,18 @@

// NEW STATE: Purpose of the AI
const [purpose, setPurpose] = useState('General Chat & Assistant');
const [backend, setBackend] = useState('Any');

const [loading, setLoading] = useState(false);
const [result, setResult] = useState('');
const [error, setError] = useState('');

// State for prediction history
const [history, setHistory] = useState(() => {
const saved = localStorage.getItem('llm_predictions_history');
return saved ? JSON.parse(saved) : [];
});

// Fetch available models from Google using the provided API Key
const handleFetchModels = async () => {
if (!customApiKey.trim()) {
Expand Down Expand Up @@ -177,11 +184,11 @@
const fetchAIPrediction = async (promptText) => {
const apiKey = customApiKey.trim() || "";

// CRITICAL FIX: If a custom key is provided, default to a standard public model (gemini-2.5-flash)
// CRITICAL FIX: If a custom key is provided, default to a standard public model (gemini-1.5-flash)
// If no key is provided, use the internal canvas preview model.
let modelName = "gemini-2.5-flash-preview-09-2025";
let modelName = "gemini-1.5-flash";
if (apiKey) {
modelName = selectedModel || "gemini-2.5-flash";
modelName = selectedModel || "gemini-1.5-flash";
}

const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${apiKey}`;
Expand Down Expand Up @@ -268,22 +275,37 @@
Model/Series: ${deviceInfo.model}
Additional Known Specs: ${deviceInfo.extraSpecs || 'None provided.'}
Primary Purpose for AI: ${purpose}
Preferred Backend Engine: ${backend}

Please generate the table of recommendations based on this setup and their intended purpose.`;
Please generate the table of recommendations based on this setup and their intended purpose. Tailor the "Best App to Run" to their preferred backend if specified.`;
} else {
prompt = `Analyze these computer specs for running Local LLMs:
OS: ${specs.os}
CPU: ${specs.cpu}
RAM: ${specs.ram}
GPU: ${specs.gpu}
Primary Purpose for AI: ${purpose}
Preferred Backend Engine: ${backend}

Please generate the table of recommendations based on this setup and their intended purpose.`;
Please generate the table of recommendations based on this setup and their intended purpose. Tailor the "Best App to Run" to their preferred backend if specified.`;
}

try {
const aiResponse = await fetchAIPrediction(prompt);
setResult(aiResponse);

// Save to history
const newHistoryItem = {
id: Date.now(),
timestamp: new Date().toLocaleString(),
type: inputMode,
details: inputMode === 'device' ? `${deviceInfo.brand} ${deviceInfo.model}` : `${specs.cpu}, ${specs.gpu}`,
result: aiResponse
};
const updatedHistory = [newHistoryItem, ...history].slice(0, 5); // Keep last 5
setHistory(updatedHistory);
localStorage.setItem('llm_predictions_history', JSON.stringify(updatedHistory));

} catch (err) {
setError(err.message || "Oops! The AI prediction failed. Please try again.");
console.error(err);
Expand Down Expand Up @@ -508,19 +530,37 @@ <h2 className="text-2xl font-bold mb-6 flex items-center text-white border-b bor
)}

{/* Purpose Selector (Applies to both modes) */}
<div className="pt-6 border-t border-slate-700/50 mt-6">
<label className="block text-sm font-medium text-slate-300 mb-2">What is the Primary Purpose for this AI?</label>
<select
value={purpose}
onChange={(e) => setPurpose(e.target.value)}
className="w-full bg-slate-800/80 border border-slate-600 rounded-xl py-3 px-4 text-white focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent transition shadow-inner"
>
<option value="General Chat & Assistant">General Chat & Assistant (Balanced)</option>
<option value="Coding & Software Development">Coding & Software Development</option>
<option value="Creative Writing & Roleplay">Creative Writing & Roleplay</option>
<option value="Data Analysis & Math">Data Analysis & Math</option>
<option value="Uncensored / Raw Output">Uncensored / Unrestricted Models</option>
</select>
<div className="pt-6 border-t border-slate-700/50 mt-6 space-y-6">
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">What is the Primary Purpose for this AI?</label>
<select
value={purpose}
onChange={(e) => setPurpose(e.target.value)}
className="w-full bg-slate-800/80 border border-slate-600 rounded-xl py-3 px-4 text-white focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent transition shadow-inner"
>
<option value="General Chat & Assistant">General Chat & Assistant (Balanced)</option>
<option value="Coding & Software Development">Coding & Software Development</option>
<option value="Creative Writing & Roleplay">Creative Writing & Roleplay</option>
<option value="Data Analysis & Math">Data Analysis & Math</option>
<option value="Uncensored / Raw Output">Uncensored / Unrestricted Models</option>
</select>
</div>

<div>
<label className="block text-sm font-medium text-slate-300 mb-2">Preferred Backend Engine</label>
<select
value={backend}
onChange={(e) => setBackend(e.target.value)}
className="w-full bg-slate-800/80 border border-slate-600 rounded-xl py-3 px-4 text-white focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent transition shadow-inner"
>
<option value="Any">Any / Don't Care</option>
<option value="Ollama">Ollama</option>
<option value="LM Studio">LM Studio</option>
<option value="GPT4All">GPT4All</option>
<option value="Text Generation WebUI">Text Generation WebUI</option>
<option value="llama.cpp">llama.cpp</option>
</select>
</div>
</div>

<div className="pt-4 border-t border-slate-700/50 mt-6">
Expand Down Expand Up @@ -581,18 +621,38 @@ <h2 className="text-2xl font-bold mb-6 flex items-center text-white border-b bor
</p>
</div>

<button
type="submit"
disabled={loading}
className={`w-full mt-8 py-4 px-4 rounded-xl font-bold text-slate-900 text-lg transition-all duration-300 transform hover:-translate-y-1 ${loading ? 'bg-slate-600 text-slate-300 cursor-not-allowed' : 'bg-brand-400 hover:bg-brand-500 shadow-[0_0_20px_rgba(16,185,129,0.3)] hover:shadow-[0_0_25px_rgba(16,185,129,0.5)]'}`}
>
{loading ? (
<span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-slate-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
Analyzing Hardware...
</span>
) : 'Generate Prediction'}
</button>
<div className="flex space-x-4 mt-8">
<button
type="button"
disabled={loading}
onClick={() => {
setResult('');
setError('');
setSpecs({
os: 'macOS',
cpu: 'Apple M1 Max',
ram: '32GB',
gpu: 'Apple Unified Memory (M-Series)'
});
setDeviceInfo({ brand: '', model: '', extraSpecs: '' });
}}
className="w-1/3 py-4 px-4 rounded-xl font-bold text-white text-lg transition-all duration-300 bg-slate-700 hover:bg-slate-600 border border-slate-600"
>
Clear
</button>
<button
type="submit"
disabled={loading}
className={`w-2/3 py-4 px-4 rounded-xl font-bold text-slate-900 text-lg transition-all duration-300 transform hover:-translate-y-1 ${loading ? 'bg-slate-600 text-slate-300 cursor-not-allowed' : 'bg-brand-400 hover:bg-brand-500 shadow-[0_0_20px_rgba(16,185,129,0.3)] hover:shadow-[0_0_25px_rgba(16,185,129,0.5)]'}`}
>
{loading ? (
<span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-slate-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
Analyzing Hardware...
</span>
) : 'Generate Prediction'}
</button>
</div>
</form>
</div>
</div>
Expand Down Expand Up @@ -638,6 +698,33 @@ <h3 className="text-2xl font-bold text-white flex items-center">
<span className="bg-brand-500 w-2 h-8 rounded-full mr-3"></span>
Analysis Report
</h3>
<div className="flex space-x-2">
<button
onClick={() => {
navigator.clipboard.writeText(result);
alert("Copied to clipboard!");
}}
className="px-3 py-1.5 text-xs font-medium bg-slate-800 hover:bg-slate-700 text-slate-300 rounded border border-slate-600 transition"
>
Copy Report
</button>
<button
onClick={() => {
const blob = new Blob([result], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'AI_Hardware_Analysis_Report.md';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}}
className="px-3 py-1.5 text-xs font-medium bg-slate-800 hover:bg-slate-700 text-brand-400 rounded border border-brand-500/30 transition"
>
Download .md
</button>
</div>
</div>

<div
Expand All @@ -651,6 +738,38 @@ <h3 className="text-2xl font-bold text-white flex items-center">
</div>
</div>

{/* History Section */}
{history.length > 0 && (
<div className="w-full max-w-6xl mb-16">
<div className="glass-panel rounded-2xl p-8 shadow-2xl">
<h3 className="text-2xl font-bold text-white mb-6 flex items-center border-b border-slate-700/50 pb-4">
<svg className="w-6 h-6 mr-3 text-brand-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
Recent Predictions
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{history.map((item) => (
<div key={item.id} className="bg-slate-800/50 border border-slate-700/50 rounded-xl p-4 hover:border-brand-500/50 transition cursor-pointer" onClick={() => setResult(item.result)}>
<div className="text-xs text-slate-400 mb-1">{item.timestamp}</div>
<div className="font-semibold text-white mb-2 truncate">{item.details}</div>
<div className="text-sm text-brand-400">Click to view report</div>
</div>
))}
</div>
<div className="mt-4 flex justify-end">
<button
onClick={() => {
setHistory([]);
localStorage.removeItem('llm_predictions_history');
}}
className="text-xs text-slate-400 hover:text-red-400 transition"
>
Clear History
</button>
</div>
</div>
</div>
)}

{/* Developer Information Section */}
<div className="w-full max-w-6xl mt-8">
<div className="bio-card rounded-2xl p-8 sm:p-10 shadow-2xl relative overflow-hidden">
Expand Down