skillsync-cli / frontend /src /components /pages /SettingsModal.tsx
Mr-Haseeb786
Clean deployment build
88da18c
Raw History Blame Contribute Delete
9.66 kB
import React from 'react';
import { motion } from 'framer-motion';
import { ToggleLeft, ToggleRight, Zap, Check } from 'lucide-react';
import { Card, Button, Select } from '@/components/common';
import Modal from '@/components/common/Modal';
import { useJobStore } from '@/store/appStore';
import { jobsAPI } from '@/utils/api';
import type { TailoringMode, LLMProvider } from '@/types';
interface SettingsModalProps {
isOpen: boolean;
onClose: () => void;
}
const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose }) => {
const {
tailoringMode,
llmProvider,
setEnableProfessionalSummary: setStoreProfessionalSummary,
setIncludeCoverLetters: setStoreCoverLetters,
setTailoringMode,
setLLMProvider,
setNotification,
} = useJobStore();
const [draftMode, setDraftMode] = React.useState<TailoringMode>(tailoringMode);
const [draftProvider, setDraftProvider] = React.useState<LLMProvider>(llmProvider || 'groq');
const [enableProfessionalSummary, setEnableProfessionalSummary] = React.useState(false);
const [includeCoverLetters, setIncludeCoverLetters] = React.useState(false);
const [isSaving, setIsSaving] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(false);
const llmProviders: Array<{ value: LLMProvider; label: string; description: string }> = [
{ value: 'ollama', label: 'Ollama (Local)', description: 'Runs locally, slowest' },
{ value: 'groq', label: 'Groq API', description: 'Fast and cost-effective' },
{ value: 'grok', label: 'Grok API', description: 'Fast and affordable' },
{ value: 'openai', label: 'OpenAI', description: 'Premium quality' },
];
React.useEffect(() => {
if (!isOpen) {
return;
}
setDraftMode(tailoringMode);
setDraftProvider(llmProvider || 'groq');
const loadConfig = async () => {
setIsLoading(true);
try {
const config = await jobsAPI.getConfig();
setDraftMode(config.tailoring_mode);
setDraftProvider(config.llm_provider);
setEnableProfessionalSummary(!!config.enable_professional_summary);
setIncludeCoverLetters(!!config.include_cover_letters);
setTailoringMode(config.tailoring_mode);
setLLMProvider(config.llm_provider);
setStoreProfessionalSummary(!!config.enable_professional_summary);
setStoreCoverLetters(!!config.include_cover_letters);
} catch (error) {
setNotification({
type: 'error',
message: 'Could not load settings. Using current values.',
});
} finally {
setIsLoading(false);
}
};
void loadConfig();
}, [
isOpen,
llmProvider,
setLLMProvider,
setNotification,
setStoreCoverLetters,
setStoreProfessionalSummary,
setTailoringMode,
tailoringMode,
]);
const handleSave = async () => {
setIsSaving(true);
try {
await jobsAPI.saveSettings({
tailoring_mode: draftMode,
llm_provider: draftProvider,
enable_professional_summary: enableProfessionalSummary,
include_cover_letters: includeCoverLetters,
});
setTailoringMode(draftMode);
setLLMProvider(draftProvider);
setStoreProfessionalSummary(enableProfessionalSummary);
setStoreCoverLetters(includeCoverLetters);
setNotification({
type: 'success',
message: 'Settings saved successfully',
});
onClose();
} catch (error) {
setNotification({
type: 'error',
message: 'Failed to save settings',
});
} finally {
setIsSaving(false);
}
};
return (
<Modal isOpen={isOpen} onClose={onClose} title="Settings" size="md">
<motion.div className="space-y-6">
{/* Tailoring Mode Section */}
<Card>
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="font-semibold text-white flex items-center gap-2">
<Zap className="w-5 h-5 text-emerald-400" />
CV Tailoring Mode
</h3>
<p className="text-xs text-slate-400 mt-1">
Choose how your CVs are tailored to job descriptions
</p>
</div>
</div>
<div className="space-y-3">
{[
{
mode: 'local' as TailoringMode,
label: 'Smart Mode (Local)',
description: 'Uses NLP-based skill matching',
icon: ToggleLeft,
},
{
mode: 'api' as TailoringMode,
label: 'AI Mode (API)',
description: 'Uses advanced AI models for better tailoring',
icon: ToggleRight,
},
].map((option) => (
<motion.button
key={option.mode}
whileHover={{ x: 4 }}
onClick={() => setDraftMode(option.mode)}
className={`w-full p-3 rounded-lg border transition-all ${
draftMode === option.mode
? 'border-emerald-500 bg-emerald-500/10'
: 'border-slate-700 bg-slate-800/30 hover:border-emerald-400/30'
}`}
disabled={isLoading || isSaving}
>
<div className="flex items-center gap-3">
<option.icon
className={`w-5 h-5 ${
draftMode === option.mode
? 'text-emerald-400'
: 'text-slate-400'
}`}
/>
<div className="text-left">
<p className="font-medium text-white text-sm">
{option.label}
</p>
<p className="text-xs text-slate-400">
{option.description}
</p>
</div>
{draftMode === option.mode && (
<div className="ml-auto w-2 h-2 bg-emerald-400 rounded-full" />
)}
</div>
</motion.button>
))}
</div>
</Card>
{/* LLM Provider Section (if API mode) */}
{draftMode === 'api' && (
<Card>
<h3 className="font-semibold text-white mb-4">
AI Model Provider
</h3>
<Select
label="Select Provider"
value={draftProvider}
onChange={(e) => setDraftProvider(e.target.value as LLMProvider)}
options={llmProviders.map((p) => ({
value: p.value,
label: `${p.label} - ${p.description}`,
}))}
/>
<p className="text-xs text-slate-400 mt-3">
Each provider has different speeds and quality levels. Get API keys
from the provider's website.
</p>
</Card>
)}
{/* Advanced Settings */}
<Card>
<h3 className="font-semibold text-white mb-4">Advanced Options</h3>
<div className="space-y-3">
{[
{
key: 'summary',
label: 'Enable Professional Summary',
description: 'Automatically generate personalized summaries',
enabled: enableProfessionalSummary,
onToggle: () => setEnableProfessionalSummary((prev) => !prev),
},
{
key: 'cover',
label: 'Include Cover Letters',
description: 'Generate cover letters along with CVs',
enabled: includeCoverLetters,
onToggle: () => setIncludeCoverLetters((prev) => !prev),
},
].map((option) => (
<motion.div
key={option.key}
whileHover={{ x: 2 }}
className={`flex items-center justify-between p-3 bg-slate-800/30 border rounded-lg transition-colors ${
option.enabled
? 'border-emerald-500/40'
: 'border-slate-700 hover:border-slate-600'
}`}
>
<div>
<p className="font-medium text-white text-sm">
{option.label}
</p>
<p className="text-xs text-slate-400">
{option.description}
</p>
</div>
<motion.button
whileTap={{ scale: 0.95 }}
onClick={option.onToggle}
disabled={isLoading || isSaving}
className={`w-7 h-7 rounded-md border-2 flex items-center justify-center transition-colors ${
option.enabled
? 'bg-emerald-500/20 border-emerald-400 text-emerald-300'
: 'border-slate-600 text-transparent hover:border-slate-500'
}`}
aria-label={`Toggle ${option.label}`}
>
<Check className="w-4 h-4" />
</motion.button>
</motion.div>
))}
</div>
</Card>
{/* Action Buttons */}
<div className="flex gap-3 pt-4 border-t border-slate-700">
<Button variant="secondary" onClick={onClose} fullWidth>
Close
</Button>
<Button onClick={handleSave} fullWidth loading={isSaving}>
Save Changes
</Button>
</div>
</motion.div>
</Modal>
);
};
export default SettingsModal;