Skip to content

Commit 5877d4c

Browse files
committed
refactor: replace regex-based model/tool matching with explicit schema properties
1 parent 890c7aa commit 5877d4c

1 file changed

Lines changed: 22 additions & 20 deletions

File tree

src/components/RecommendationWizard.tsx

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ interface PlanData {
2525
average_ms?: number;
2626
uptime_percent?: number;
2727
};
28+
has_open_weight?: boolean;
29+
has_closed_source?: boolean;
30+
has_cli?: boolean;
31+
has_ide?: boolean;
2832
}
2933

3034
interface Props {
@@ -48,17 +52,6 @@ interface ScoredPlan {
4852
reasons: string[];
4953
}
5054

51-
const OPEN_WEIGHT_PATTERNS = [
52-
'deepseek', 'mistral', 'llama', 'qwen', 'ollama', 'glm', 'kimi', 'minimax',
53-
];
54-
55-
const CLOSED_SOURCE_PATTERNS = [
56-
'gpt', 'claude', 'gemini', 'copilot',
57-
];
58-
59-
const CLI_PATTERNS = ['cli', 'claude-code', 'claudes-code', 'opencode', 'terminal', 'command-line', 'gemini-cli', 'aider', 'codex'];
60-
const IDE_PATTERNS = ['cursor', 'vscode', 'vs code', 'jetbrains', 'intellij', 'pycharm', 'webstorm', 'idea', 'edlide', 'kiro'];
61-
6255
function isUnlimited(val: number | string | null | undefined): boolean {
6356
if (val == null) return false;
6457
if (typeof val === 'string') return val.toLowerCase().includes('unlimited');
@@ -89,10 +82,16 @@ function scorePlan(plan: PlanData, state: FormState): ScoredPlan {
8982
score += Math.round(headroom * 25);
9083
if (headroom >= 1) reasons.push(`${(tpm / 1000).toFixed(0)}k tokens/min — fits your usage`);
9184
else reasons.push(`${(tpm / 1000).toFixed(0)}k tokens/min — may be tight`);
85+
} else {
86+
score += 25; // Unknown/dynamic limit, assume it fits to avoid penalizing
87+
reasons.push('Dynamic token limits');
9288
}
9389
if (cw != null) {
9490
score += Math.min(15, Math.round((cw / 200_000) * 15));
9591
if (cw >= 128_000) reasons.push(`${(cw / 1000).toFixed(0)}k context window`);
92+
} else {
93+
score += 15;
94+
reasons.push('Dynamic context window');
9695
}
9796
} else {
9897
const rpm = plan.limits.requests_per_minute;
@@ -103,6 +102,9 @@ function scorePlan(plan: PlanData, state: FormState): ScoredPlan {
103102
score += Math.round(headroom * 25);
104103
if (headroom >= 1) reasons.push(`${rpm} requests/min — fits your usage`);
105104
else reasons.push(`${rpm} requests/min — may be tight`);
105+
} else {
106+
score += 25; // Dynamic limits
107+
reasons.push(`Dynamic request limits`);
106108
}
107109
if (isUnlimited(daily)) {
108110
score += 15;
@@ -112,6 +114,9 @@ function scorePlan(plan: PlanData, state: FormState): ScoredPlan {
112114
const headroom = Math.min(1, daily / Math.max(dayTotal, 1));
113115
score += Math.round(headroom * 15);
114116
if (headroom >= 1) reasons.push(`${daily} messages/day — enough for you`);
117+
} else if (daily == null) {
118+
score += 15;
119+
reasons.push('Dynamic daily messages');
115120
}
116121
}
117122
}
@@ -120,12 +125,10 @@ function scorePlan(plan: PlanData, state: FormState): ScoredPlan {
120125
// Tool type (0-30 pts) — CLI vs IDE
121126
if (state.tool) {
122127
maxScore += 30;
123-
const toolStr = plan.tools_compatible.join(' ').toLowerCase();
124-
const patterns = state.tool === 'cli' ? CLI_PATTERNS : IDE_PATTERNS;
125128
const label = state.tool === 'cli' ? 'CLI' : 'IDE';
129+
const hasMatch = state.tool === 'cli' ? plan.has_cli : plan.has_ide;
126130

127-
const matches = patterns.filter(p => toolStr.includes(p)).length;
128-
if (matches > 0) {
131+
if (hasMatch) {
129132
score += 30;
130133
reasons.push(`Compatible with ${label}`);
131134
} else {
@@ -136,12 +139,10 @@ function scorePlan(plan: PlanData, state: FormState): ScoredPlan {
136139
// Model preference (0-30 pts) — open weight vs closed source
137140
if (state.modelPref) {
138141
maxScore += 30;
139-
const modelStr = plan.models.join(' ').toLowerCase();
140-
const patterns = state.modelPref === 'open_weight' ? OPEN_WEIGHT_PATTERNS : CLOSED_SOURCE_PATTERNS;
141142
const label = state.modelPref === 'open_weight' ? 'open-weight' : 'closed-source';
143+
const hasMatch = state.modelPref === 'open_weight' ? plan.has_open_weight : plan.has_closed_source;
142144

143-
const matched = patterns.filter(p => modelStr.includes(p));
144-
if (matched.length > 0) {
145+
if (hasMatch) {
145146
score += 30;
146147
reasons.push(`Has ${label} models`);
147148
} else {
@@ -207,7 +208,8 @@ export default function RecommendationWizard({ plans }: Props) {
207208

208209
{/* Row 1: Usage Intensity */}
209210
<div className="space-y-3">
210-
<h3 className="font-semibold text-sm">Usage Intensity (5 hrs/day)</h3>
211+
<h3 className="font-semibold text-sm">Average Usage Intensity (5 hrs/day)</h3>
212+
<p className="text-xs text-base-content/60">Input your *average* expected usage for a 5-hour window, not absolute burst limits.</p>
211213
<div className="grid grid-cols-2 gap-3">
212214
{([
213215
{ value: 'token_avg' as UsageIntensity, label: 'Token Average', placeholder: 'e.g. 50000', icon: '🔢' },

0 commit comments

Comments
 (0)