Skip to content

Commit 3af16c2

Browse files
feat(desktop): allow custom onboarding model ids
Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
1 parent 2575478 commit 3af16c2

3 files changed

Lines changed: 114 additions & 26 deletions

File tree

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import { PROVIDER_SHORTLIST, type SupportedOnboardingProvider } from '@open-codesign/shared';
22
import { Button } from '@open-codesign/ui';
3-
import { useState } from 'react';
3+
import { useEffect, useId, useState } from 'react';
4+
5+
const OPENROUTER_FREE_MODEL = 'openrouter/free';
46

57
interface ChooseModelProps {
68
provider: SupportedOnboardingProvider;
9+
preferFreeTier?: boolean;
10+
baseUrl: string | null;
711
saving: boolean;
812
errorMessage: string | null;
913
onConfirm: (modelPrimary: string, modelFast: string) => void;
@@ -12,14 +16,32 @@ interface ChooseModelProps {
1216

1317
export function ChooseModel({
1418
provider,
19+
preferFreeTier = false,
20+
baseUrl,
1521
saving,
1622
errorMessage,
1723
onConfirm,
1824
onBack,
1925
}: ChooseModelProps) {
2026
const shortlist = PROVIDER_SHORTLIST[provider];
21-
const [modelPrimary, setModelPrimary] = useState(shortlist.defaultPrimary);
22-
const [modelFast, setModelFast] = useState(shortlist.defaultFast);
27+
const useFreeTierDefaults = provider === 'openrouter' && preferFreeTier;
28+
const primaryOptions = withFreeTierSuggestion(shortlist.primary, useFreeTierDefaults);
29+
const fastOptions = withFreeTierSuggestion(shortlist.fast, useFreeTierDefaults);
30+
const [modelPrimary, setModelPrimary] = useState(
31+
getDefaultModel(shortlist.defaultPrimary, useFreeTierDefaults),
32+
);
33+
const [modelFast, setModelFast] = useState(
34+
getDefaultModel(shortlist.defaultFast, useFreeTierDefaults),
35+
);
36+
37+
useEffect(() => {
38+
setModelPrimary(getDefaultModel(shortlist.defaultPrimary, useFreeTierDefaults));
39+
setModelFast(getDefaultModel(shortlist.defaultFast, useFreeTierDefaults));
40+
}, [shortlist.defaultPrimary, shortlist.defaultFast, useFreeTierDefaults]);
41+
42+
const trimmedPrimary = modelPrimary.trim();
43+
const trimmedFast = modelFast.trim();
44+
const canFinish = trimmedPrimary.length > 0 && trimmedFast.length > 0 && !saving;
2345

2446
return (
2547
<div className="flex flex-col gap-5">
@@ -28,27 +50,44 @@ export function ChooseModel({
2850
Pick default models
2951
</h2>
3052
<p className="text-[14px] text-[var(--color-text-secondary)] leading-[1.55]">
31-
Recommended starters for {shortlist.label}. Switchable per-design later.
53+
Start with a recommendation or enter any provider-specific model ID. You can switch these
54+
per design later.
3255
</p>
3356
</div>
3457

3558
<ModelPicker
3659
label="Primary design model"
37-
hint="Used for full design generation."
60+
hint={
61+
useFreeTierDefaults
62+
? 'Free path starts on openrouter/free, but you can enter any OpenRouter model ID.'
63+
: 'Used for full design generation.'
64+
}
3865
value={modelPrimary}
39-
options={shortlist.primary}
66+
options={primaryOptions}
4067
onChange={setModelPrimary}
4168
/>
4269
<ModelPicker
4370
label="Fast completion model"
44-
hint="Used for quick edits and inline tweaks."
71+
hint={
72+
useFreeTierDefaults
73+
? 'Keep openrouter/free for lowest cost, or replace it with a faster custom choice.'
74+
: 'Used for quick edits and inline tweaks.'
75+
}
4576
value={modelFast}
46-
options={shortlist.fast}
77+
options={fastOptions}
4778
onChange={setModelFast}
4879
/>
4980

81+
{baseUrl !== null ? (
82+
<p className="text-[12px] text-[var(--color-text-muted)] leading-[1.5]">
83+
Custom base URL: <span style={{ fontFamily: 'var(--font-mono)' }}>{baseUrl}</span>
84+
</p>
85+
) : null}
86+
5087
<p className="text-[12px] text-[var(--color-text-muted)] leading-[1.5]">
51-
Estimated cost: ~$0.01–0.05 per design session (varies by provider and prompt length).
88+
{useFreeTierDefaults
89+
? 'OpenRouter free routing availability can change. If a free route is unavailable, type another model ID here.'
90+
: 'Estimated cost varies by provider, chosen model, and prompt length.'}
5291
</p>
5392

5493
{errorMessage !== null ? (
@@ -62,10 +101,10 @@ export function ChooseModel({
62101
<Button
63102
type="button"
64103
variant="primary"
65-
onClick={() => onConfirm(modelPrimary, modelFast)}
66-
disabled={saving}
104+
onClick={() => onConfirm(trimmedPrimary, trimmedFast)}
105+
disabled={!canFinish}
67106
>
68-
{saving ? 'Saving' : 'Finish'}
107+
{saving ? 'Saving...' : 'Finish'}
69108
</Button>
70109
</div>
71110
</div>
@@ -81,27 +120,68 @@ interface ModelPickerProps {
81120
}
82121

83122
function ModelPicker({ label, hint, value, options, onChange }: ModelPickerProps) {
123+
const inputId = useId();
124+
const datalistId = useId();
125+
84126
return (
85-
<label className="flex flex-col gap-2">
86-
<span
127+
<div className="flex flex-col gap-2">
128+
<label
129+
htmlFor={inputId}
87130
className="text-[10px] uppercase tracking-[0.08em] text-[var(--color-text-muted)] font-medium"
88131
style={{ fontFamily: 'var(--font-mono)' }}
89132
>
90133
{label}
91-
</span>
92-
<select
134+
</label>
135+
136+
<input
137+
id={inputId}
138+
type="text"
93139
value={value}
140+
list={datalistId}
94141
onChange={(e) => onChange(e.target.value)}
142+
placeholder={options[0]}
143+
spellCheck={false}
95144
style={{ fontFamily: 'var(--font-mono)' }}
96-
className="w-full h-[40px] px-3 rounded-[var(--radius-md)] bg-[var(--color-surface)] border border-[var(--color-border)] text-[13px] text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-accent)] focus:shadow-[0_0_0_3px_var(--color-focus-ring)] transition-[box-shadow,border-color] duration-150 ease-[cubic-bezier(0.16,1,0.3,1)] cursor-pointer"
97-
>
145+
className="w-full h-[40px] px-3 rounded-[var(--radius-md)] bg-[var(--color-surface)] border border-[var(--color-border)] text-[13px] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:border-[var(--color-accent)] focus:shadow-[0_0_0_3px_var(--color-focus-ring)] transition-[box-shadow,border-color] duration-150 ease-[cubic-bezier(0.16,1,0.3,1)]"
146+
/>
147+
<datalist id={datalistId}>
98148
{options.map((opt) => (
99-
<option key={opt} value={opt}>
100-
{opt}
101-
</option>
149+
<option key={opt} value={opt} />
102150
))}
103-
</select>
151+
</datalist>
152+
153+
<div className="flex flex-wrap gap-2">
154+
{options.map((opt) => {
155+
const selected = value.trim() === opt;
156+
157+
return (
158+
<button
159+
key={opt}
160+
type="button"
161+
onClick={() => onChange(opt)}
162+
className={`px-2.5 h-[28px] rounded-full border text-[11px] transition-colors ${
163+
selected
164+
? 'border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-accent)]'
165+
: 'border-[var(--color-border)] text-[var(--color-text-secondary)] hover:border-[var(--color-border-strong)] hover:text-[var(--color-text-primary)]'
166+
}`}
167+
style={{ fontFamily: 'var(--font-mono)' }}
168+
>
169+
{opt}
170+
</button>
171+
);
172+
})}
173+
</div>
174+
104175
<span className="text-[12px] text-[var(--color-text-muted)] leading-[1.4]">{hint}</span>
105-
</label>
176+
</div>
106177
);
107178
}
179+
180+
function getDefaultModel(defaultModel: string, useFreeTierDefaults: boolean): string {
181+
return useFreeTierDefaults ? OPENROUTER_FREE_MODEL : defaultModel;
182+
}
183+
184+
function withFreeTierSuggestion(options: string[], useFreeTierDefaults: boolean): string[] {
185+
if (!useFreeTierDefaults) return options;
186+
return [OPENROUTER_FREE_MODEL, ...options.filter((opt) => opt !== OPENROUTER_FREE_MODEL)];
187+
}

apps/desktop/src/renderer/src/onboarding/Welcome.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function Welcome({ onPickKey, onPickFreeTier, ollamaDetected }: WelcomePr
2323
<PathButton
2424
icon={<Rocket className="w-[18px] h-[18px]" />}
2525
title="Try free now"
26-
subtitle="OpenRouter free tier paste an OpenRouter key, then pick a free model."
26+
subtitle="OpenRouter free tier - paste an OpenRouter key, then start with openrouter/free or type any model ID."
2727
onClick={onPickFreeTier}
2828
/>
2929
<PathButton
@@ -36,7 +36,7 @@ export function Welcome({ onPickKey, onPickFreeTier, ollamaDetected }: WelcomePr
3636
<PathButton
3737
icon={<Server className="w-[18px] h-[18px]" />}
3838
title="Use local model (Ollama detected)"
39-
subtitle="Coming in v0.2 Ollama integration is on the roadmap."
39+
subtitle="Coming in v0.2 - Ollama integration is on the roadmap."
4040
disabled
4141
/>
4242
) : null}

apps/desktop/src/renderer/src/onboarding/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ export function Onboarding() {
1212
const completeOnboarding = useCodesignStore((s) => s.completeOnboarding);
1313
const [step, setStep] = useState<Step>('welcome');
1414
const [provider, setProvider] = useState<SupportedOnboardingProvider | null>(null);
15+
const [preferFreeTier, setPreferFreeTier] = useState(false);
1516
const [apiKey, setApiKey] = useState('');
1617
const [baseUrl, setBaseUrl] = useState<string | null>(null);
1718
const [saving, setSaving] = useState(false);
1819
const [errorMessage, setErrorMessage] = useState<string | null>(null);
1920

2021
function handleValidated(p: SupportedOnboardingProvider, key: string, url: string | null) {
2122
setProvider(p);
23+
setPreferFreeTier((current) => (p === 'openrouter' ? current : false));
2224
setApiKey(key);
2325
setBaseUrl(url);
2426
setStep('model');
@@ -68,8 +70,12 @@ export function Onboarding() {
6870

6971
{step === 'welcome' ? (
7072
<Welcome
71-
onPickKey={() => setStep('paste')}
73+
onPickKey={() => {
74+
setPreferFreeTier(false);
75+
setStep('paste');
76+
}}
7277
onPickFreeTier={() => {
78+
setPreferFreeTier(true);
7379
setProvider('openrouter');
7480
setStep('paste');
7581
}}
@@ -82,6 +88,8 @@ export function Onboarding() {
8288
{step === 'model' && provider !== null ? (
8389
<ChooseModel
8490
provider={provider}
91+
preferFreeTier={preferFreeTier}
92+
baseUrl={baseUrl}
8593
saving={saving}
8694
errorMessage={errorMessage}
8795
onConfirm={handleConfirm}

0 commit comments

Comments
 (0)