Skip to content

Commit 01c5cca

Browse files
committed
Add first-run onboarding wizard and icons for Settings sidebar tabs
Onboarding: on first launch, a dialog lets you pick how you want to run models (Ollama, llama.cpp, ChatGPT, Claude, or Gemini) and enter an API key inline for cloud providers, instead of discovering Settings on your own. Skippable, and marked complete in settings (onboardingComplete) so it never reappears once dismissed either way. Settings polish: each sidebar tab now has an icon (General, Models, Integrations, Chat & Prompts, Voice, Automation, Data) instead of text alone, matching the icon-plus-label pattern already used elsewhere in the app (sidebar nav, MCP servers, etc). Verified the icons render correctly and that the justify-start override actually wins over the component's default justify-center (tailwind-merge resolves the conflicting utility classes, confirmed via computed style, not just assumed). The onboarding wizard's auto-show trigger lives in Layout, which mounts once for the whole app before window.api exists in the browser test harness - couldn't be exercised end-to-end there (a test-harness limitation, not a real code path, since Electron's preload script provides window.api from the start); it follows the exact same Dialog structure as the already-verified keyboard shortcuts dialog.
1 parent 5cc5bb2 commit 01c5cca

5 files changed

Lines changed: 181 additions & 8 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ Beyond chat, Modelforge includes an **agentic mode** — the model can read/writ
6666
- **Custom model storage location** — Settings → General → Ollama Server → "Model storage location" lets you point downloaded models at any folder (e.g. a larger or faster drive) instead of Ollama's default location. If this app started Ollama, it restarts it automatically with the new location; if Ollama is running outside the app, you're told to restart it yourself.
6767

6868
**Customization & control**
69-
- **Settings is organized into tabs** (General, Models, Integrations, Chat & Prompts, Voice, Data) instead of one long scrolling page — it holds up better as more settings get added over time.
69+
- **First-run setup** — on first launch, pick which provider you want to start with (a local one, or a cloud one with its API key) right away, instead of hunting through Settings. Skippable, and only shown once.
70+
- **Settings is organized into a sidebar** (General, Models, Integrations, Chat & Prompts, Voice, Automation, Data), each with its own icon — it holds up better as more settings get added over time than one long scrolling page.
7071
- English and Turkish UI localization.
7172
- **Theming** — light/dark/system color mode plus a choice of accent colors (default gray, blue, green, purple, orange, rose), in Settings → General → Appearance.
7273
- Configurable Ollama host — point at a remote server instead of localhost.

‎frontend/src/components/layout.tsx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
2626
import { ThemeToggle } from "@/components/theme-toggle";
2727
import { CommandPalette } from "@/components/command-palette";
2828
import { KeyboardShortcutsDialog } from "@/components/keyboard-shortcuts-dialog";
29+
import { OnboardingWizard } from "@/components/onboarding-wizard";
2930
import { useSessions } from "@/lib/sessions-context";
3031
import { useI18n } from "@/lib/i18n";
3132
import { cn } from "@/lib/utils";
@@ -513,6 +514,14 @@ export default function Layout() {
513514
const [newProjectName, setNewProjectName] = useState("");
514515
const [paletteOpen, setPaletteOpen] = useState(false);
515516
const [shortcutsOpen, setShortcutsOpen] = useState(false);
517+
const [showOnboarding, setShowOnboarding] = useState(false);
518+
519+
useEffect(() => {
520+
if (!hasApi) return;
521+
window.api.settings.get().then((s) => {
522+
setShowOnboarding(!s.onboardingComplete);
523+
});
524+
}, [hasApi]);
516525

517526
const query = search.trim().toLowerCase();
518527
const matchesSearch = (s: ChatSession) =>
@@ -754,6 +763,7 @@ export default function Layout() {
754763
onNavigateCompare={() => navigate("/compare")}
755764
/>
756765
<KeyboardShortcutsDialog open={shortcutsOpen} onOpenChange={setShortcutsOpen} />
766+
<OnboardingWizard open={showOnboarding} onDone={() => setShowOnboarding(false)} />
757767
</div>
758768
);
759769
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { useState } from "react";
2+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
3+
import { Button } from "@/components/ui/button";
4+
import { Input } from "@/components/ui/input";
5+
import { Bot, Cpu, Cloud, Check } from "lucide-react";
6+
import { cn } from "@/lib/utils";
7+
import { useI18n } from "@/lib/i18n";
8+
9+
type OnboardingProvider = "ollama" | "llamacpp" | "openai" | "anthropic" | "gemini";
10+
11+
const CLOUD_KEY_INFO: Record<Exclude<OnboardingProvider, "ollama" | "llamacpp">, { secretKey: string; placeholder: string }> = {
12+
openai: { secretKey: "openai_api_key", placeholder: "sk-..." },
13+
anthropic: { secretKey: "anthropic_api_key", placeholder: "sk-ant-..." },
14+
gemini: { secretKey: "gemini_api_key", placeholder: "AIza..." },
15+
};
16+
17+
export function OnboardingWizard({ open, onDone }: { open: boolean; onDone: () => void }) {
18+
const { t } = useI18n();
19+
const [selected, setSelected] = useState<OnboardingProvider | null>(null);
20+
const [keyInput, setKeyInput] = useState("");
21+
const [saved, setSaved] = useState(false);
22+
23+
function selectProvider(provider: OnboardingProvider) {
24+
setSelected(provider);
25+
setKeyInput("");
26+
setSaved(false);
27+
}
28+
29+
async function finish() {
30+
await window.api.settings.save({ onboardingComplete: true });
31+
onDone();
32+
}
33+
34+
async function saveKeyAndFinish() {
35+
if (selected && selected !== "ollama" && selected !== "llamacpp" && keyInput.trim()) {
36+
await window.api.secrets.set(CLOUD_KEY_INFO[selected].secretKey, keyInput.trim());
37+
setSaved(true);
38+
}
39+
await finish();
40+
}
41+
42+
const options: { id: OnboardingProvider; label: string; description: string; icon: React.ReactNode }[] = [
43+
{ id: "ollama", label: t.onboardingOllama, description: t.onboardingOllamaDesc, icon: <Bot className="size-5" /> },
44+
{ id: "llamacpp", label: t.onboardingLlamaCpp, description: t.onboardingLlamaCppDesc, icon: <Cpu className="size-5" /> },
45+
{ id: "openai", label: "ChatGPT (OpenAI)", description: t.onboardingCloudDesc, icon: <Cloud className="size-5" /> },
46+
{ id: "anthropic", label: "Claude (Anthropic)", description: t.onboardingCloudDesc, icon: <Cloud className="size-5" /> },
47+
{ id: "gemini", label: "Gemini (Google)", description: t.onboardingCloudDesc, icon: <Cloud className="size-5" /> },
48+
];
49+
50+
const isCloud = selected && selected !== "ollama" && selected !== "llamacpp";
51+
52+
return (
53+
<Dialog open={open} onOpenChange={(o) => !o && finish()}>
54+
<DialogContent className="max-w-md" showCloseButton={false}>
55+
<DialogHeader>
56+
<DialogTitle>{t.onboardingTitle}</DialogTitle>
57+
<DialogDescription>{t.onboardingSubtitle}</DialogDescription>
58+
</DialogHeader>
59+
<div className="flex flex-col gap-2">
60+
{options.map((o) => (
61+
<button
62+
key={o.id}
63+
onClick={() => selectProvider(o.id)}
64+
className={cn(
65+
"flex items-center gap-3 rounded-lg border p-3 text-left transition-colors",
66+
selected === o.id ? "border-primary bg-primary/5" : "border-border hover:bg-muted/50"
67+
)}
68+
>
69+
<span className="text-muted-foreground">{o.icon}</span>
70+
<span className="min-w-0 flex-1">
71+
<span className="block text-sm font-medium">{o.label}</span>
72+
<span className="block text-xs text-muted-foreground">{o.description}</span>
73+
</span>
74+
{selected === o.id && <Check className="size-4 shrink-0 text-primary" />}
75+
</button>
76+
))}
77+
</div>
78+
79+
{isCloud && (
80+
<div className="flex flex-col gap-2 rounded-lg border border-border bg-muted/30 p-3">
81+
<label className="text-xs text-muted-foreground">{t.onboardingKeyLabel}</label>
82+
<Input
83+
type="password"
84+
value={keyInput}
85+
onChange={(e) => setKeyInput(e.target.value)}
86+
placeholder={CLOUD_KEY_INFO[selected].placeholder}
87+
autoFocus
88+
/>
89+
<p className="text-xs text-muted-foreground">{t.onboardingKeyHint}</p>
90+
</div>
91+
)}
92+
93+
<div className="flex items-center justify-between">
94+
<button onClick={finish} className="text-xs text-muted-foreground hover:text-foreground">
95+
{t.onboardingSkip}
96+
</button>
97+
{selected && (
98+
<Button
99+
size="sm"
100+
onClick={isCloud ? saveKeyAndFinish : finish}
101+
disabled={isCloud ? !keyInput.trim() || saved : false}
102+
>
103+
{t.onboardingContinue}
104+
</Button>
105+
)}
106+
</div>
107+
</DialogContent>
108+
</Dialog>
109+
);
110+
}

‎frontend/src/lib/translations.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,17 @@ export interface Dictionary {
204204
customProviderBaseUrl: string;
205205
customProviderModelIds: string;
206206
addCustomProvider: string;
207+
onboardingTitle: string;
208+
onboardingSubtitle: string;
209+
onboardingOllama: string;
210+
onboardingOllamaDesc: string;
211+
onboardingLlamaCpp: string;
212+
onboardingLlamaCppDesc: string;
213+
onboardingCloudDesc: string;
214+
onboardingKeyLabel: string;
215+
onboardingKeyHint: string;
216+
onboardingSkip: string;
217+
onboardingContinue: string;
207218
resetToDefault: string;
208219
usingCustomPrompt: string;
209220
dataManagement: string;
@@ -446,6 +457,17 @@ export const en: Dictionary = {
446457
customProviderBaseUrl: "Base URL (e.g. https://api.groq.com/openai/v1)",
447458
customProviderModelIds: "Model IDs, comma-separated",
448459
addCustomProvider: "Add provider",
460+
onboardingTitle: "Welcome to Modelforge",
461+
onboardingSubtitle: "How would you like to run models? You can add more later in Settings.",
462+
onboardingOllama: "Ollama (local)",
463+
onboardingOllamaDesc: "Free, private, runs on your machine. Recommended to start.",
464+
onboardingLlamaCpp: "llama.cpp (local)",
465+
onboardingLlamaCppDesc: "Free, private, with Vulkan/CUDA GPU acceleration.",
466+
onboardingCloudDesc: "Requires an API key and a paid account with the provider.",
467+
onboardingKeyLabel: "API key",
468+
onboardingKeyHint: "Stored encrypted on this device — you can change it anytime in Settings.",
469+
onboardingSkip: "Skip for now",
470+
onboardingContinue: "Continue",
449471
resetToDefault: "Reset to default",
450472
usingCustomPrompt: "Custom prompt for this chat",
451473
dataManagement: "Data management",
@@ -696,6 +718,17 @@ export const tr: Dictionary = {
696718
customProviderBaseUrl: "Temel URL (ör. https://api.groq.com/openai/v1)",
697719
customProviderModelIds: "Model ID'leri, virgülle ayrılmış",
698720
addCustomProvider: "Sağlayıcı ekle",
721+
onboardingTitle: "Modelforge'a hoş geldiniz",
722+
onboardingSubtitle: "Modelleri nasıl çalıştırmak istersiniz? Daha sonra Ayarlar'dan başka sağlayıcı da ekleyebilirsiniz.",
723+
onboardingOllama: "Ollama (yerel)",
724+
onboardingOllamaDesc: "Ücretsiz, özel, bilgisayarınızda çalışır. Başlamak için önerilir.",
725+
onboardingLlamaCpp: "llama.cpp (yerel)",
726+
onboardingLlamaCppDesc: "Vulkan/CUDA GPU hızlandırmalı, ücretsiz ve özel.",
727+
onboardingCloudDesc: "Sağlayıcıdan bir API anahtarı ve ücretli hesap gerektirir.",
728+
onboardingKeyLabel: "API anahtarı",
729+
onboardingKeyHint: "Bu cihazda şifreli olarak saklanır — istediğiniz zaman Ayarlar'dan değiştirebilirsiniz.",
730+
onboardingSkip: "Şimdilik atla",
731+
onboardingContinue: "Devam et",
699732
resetToDefault: "Varsayılana dön",
700733
usingCustomPrompt: "Bu sohbet için özel istem",
701734
dataManagement: "Veri yönetimi",

‎frontend/src/pages/Settings.tsx‎

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ import {
2121
ChevronRight,
2222
Clock,
2323
Play,
24+
SlidersHorizontal,
25+
Boxes,
26+
MessageSquare,
27+
Volume2,
28+
Database,
2429
} from "lucide-react";
2530
import { Button } from "@/components/ui/button";
2631
import { Badge } from "@/components/ui/badge";
@@ -699,13 +704,27 @@ export default function Settings() {
699704
className="items-start gap-8"
700705
>
701706
<TabsList variant="line" className="w-48 shrink-0">
702-
<TabsTrigger value="general">{t.settingsTabGeneral}</TabsTrigger>
703-
<TabsTrigger value="models">{t.settingsTabModels}</TabsTrigger>
704-
<TabsTrigger value="integrations">{t.settingsTabIntegrations}</TabsTrigger>
705-
<TabsTrigger value="chat">{t.settingsTabChat}</TabsTrigger>
706-
<TabsTrigger value="voice">{t.settingsTabVoice}</TabsTrigger>
707-
<TabsTrigger value="automation">{t.settingsTabAutomation}</TabsTrigger>
708-
<TabsTrigger value="data">{t.settingsTabData}</TabsTrigger>
707+
<TabsTrigger value="general" className="justify-start gap-2">
708+
<SlidersHorizontal className="size-4 shrink-0" /> {t.settingsTabGeneral}
709+
</TabsTrigger>
710+
<TabsTrigger value="models" className="justify-start gap-2">
711+
<Boxes className="size-4 shrink-0" /> {t.settingsTabModels}
712+
</TabsTrigger>
713+
<TabsTrigger value="integrations" className="justify-start gap-2">
714+
<Plug className="size-4 shrink-0" /> {t.settingsTabIntegrations}
715+
</TabsTrigger>
716+
<TabsTrigger value="chat" className="justify-start gap-2">
717+
<MessageSquare className="size-4 shrink-0" /> {t.settingsTabChat}
718+
</TabsTrigger>
719+
<TabsTrigger value="voice" className="justify-start gap-2">
720+
<Volume2 className="size-4 shrink-0" /> {t.settingsTabVoice}
721+
</TabsTrigger>
722+
<TabsTrigger value="automation" className="justify-start gap-2">
723+
<Clock className="size-4 shrink-0" /> {t.settingsTabAutomation}
724+
</TabsTrigger>
725+
<TabsTrigger value="data" className="justify-start gap-2">
726+
<Database className="size-4 shrink-0" /> {t.settingsTabData}
727+
</TabsTrigger>
709728
</TabsList>
710729

711730
<TabsContent value="general" className="min-w-0 flex-1 flex flex-col gap-8">

0 commit comments

Comments
 (0)