Skip to content

Commit a7e3acd

Browse files
committed
Use GPT-OSS-120B free model with manual insight controls
Enforces the free 120B model across all AI calls and removes legacy aiInsights preference. Replaces automatic daily analysis with explicit “Analyze latest entry” and “Run 1-week analysis” buttons. Adds server‐side proxy endpoints for validation and analysis, strengthens JSON parsing/sanitization, tracks daily rate-limit exhaustion, and surfaces toast notifications for better feedback.
1 parent 88331dd commit a7e3acd

File tree

12 files changed

+492
-214
lines changed

12 files changed

+492
-214
lines changed

src/lib/components/Layout.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
$: ruixenState = (() => {
1919
const hasKey = Boolean($guardianStore?.apiKey) && Boolean($guardianStore?.apiKeyValid);
2020
if (!hasKey) return { label: 'Needs API key', color: '--petalytics-love' };
21-
const model = ($guardianStore as any)?.model || 'openai/gpt-oss-20b:free';
21+
const model = 'openai/gpt-oss-120b:free';
2222
const isFree = /:free$/i.test(model);
2323
const daily = Number($ruixenDaily || 0);
2424
const q = Number($ruixenQueue || 0);

src/lib/components/panels/GuardianPanel.svelte

Lines changed: 9 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@
1111
let prevApiKey = '';
1212
let apiKeyEl: HTMLInputElement | null = null;
1313
let guardianName = '';
14-
// Preferences trimmed to real features; remove unused reminders/notifications.
15-
let preferences: { aiInsights: boolean } = {
16-
aiInsights: true,
17-
};
1814
1915
let apiKeyStatus = 'unchecked'; // unchecked, checking, valid, invalid
2016
@@ -28,6 +24,7 @@
2824
2925
// OpenRouter model selection (dynamic; fallback list shown until fetched)
3026
let modelList: string[] = [
27+
'openai/gpt-oss-120b:free',
3128
'openai/gpt-oss-20b:free',
3229
'mistralai/mixtral-8x7b-instruct:free',
3330
'google/gemma-2-9b-it:free',
@@ -95,7 +92,6 @@
9592
if (saved) {
9693
guardianName = saved.name || '';
9794
apiKeyInput = saved.apiKey || '';
98-
preferences = { ...preferences, ...saved.preferences };
9995
}
10096
10197
// Get current theme from localStorage
@@ -121,12 +117,6 @@
121117
loadThemePreset(currentThemeKey);
122118
}
123119
124-
function togglePreference(key: keyof typeof preferences) {
125-
// currently only ai_insights is supported
126-
(preferences as any)[key] = !(preferences as any)[key];
127-
saveGuardianInfo();
128-
}
129-
130120
function startEdit(field: string) {
131121
if (field === 'apiKey') {
132122
// Prepare for paste-first UX: clear input and focus the field
@@ -172,25 +162,17 @@
172162
apiKeyStatus = 'checking';
173163
174164
try {
175-
// First try the AI analysis helper for more direct validation
176-
const isValid = await aiAnalysisHelpers.testConnection();
177-
if (isValid) {
165+
const response = await fetch('/api/ai/validate', {
166+
method: 'POST',
167+
headers: { 'Content-Type': 'application/json' },
168+
body: JSON.stringify({ apiKey: apiKeyInput }),
169+
});
170+
171+
if (response.ok) {
178172
apiKeyStatus = 'valid';
179173
guardianHelpers.updateApiKey(apiKeyInput);
180174
} else {
181-
// Fallback to backend validation if direct test fails
182-
const response = await fetch('/api/ai/validate', {
183-
method: 'POST',
184-
headers: { 'Content-Type': 'application/json' },
185-
body: JSON.stringify({ apiKey: apiKeyInput }),
186-
});
187-
188-
if (response.ok) {
189-
apiKeyStatus = 'valid';
190-
guardianHelpers.updateApiKey(apiKeyInput);
191-
} else {
192-
apiKeyStatus = 'invalid';
193-
}
175+
apiKeyStatus = 'invalid';
194176
}
195177
} catch (error) {
196178
apiKeyStatus = 'invalid';
@@ -201,12 +183,9 @@
201183
guardianHelpers.update({
202184
name: guardianName,
203185
apiKey: apiKeyInput,
204-
preferences: preferences,
205186
});
206187
}
207188
208-
// removed handlePreferenceChange; inline save in togglePreference
209-
210189
// Keyboard activate handler for elements with role="button"
211190
function handleActivate(e: KeyboardEvent, action: () => void) {
212191
if (e.key === 'Enter' || e.key === ' ') {
@@ -354,34 +333,6 @@
354333
</div>
355334
</div>
356335

357-
<!-- Preferences: only ai_insights -->
358-
<div
359-
class="cli-row px-2 py-1"
360-
role="button"
361-
tabindex="0"
362-
aria-pressed={preferences.aiInsights}
363-
onclick={() => togglePreference('aiInsights')}
364-
onkeydown={(e) => handleActivate(e, () => togglePreference('aiInsights'))}
365-
>
366-
<span class="label" style="color: var(--petalytics-foam);">ai_insights</span>
367-
<span class="value" style="color: var(--petalytics-text);">
368-
{#if preferences.aiInsights}
369-
{guardianHelpers.load()?.apiKeyValid ? 'cloud' : 'offline'}
370-
{:else}
371-
offline
372-
{/if}
373-
</span>
374-
<span
375-
class="ml-2"
376-
style="color: {preferences.aiInsights
377-
? guardianHelpers.load()?.apiKeyValid
378-
? 'var(--petalytics-iris)'
379-
: 'var(--petalytics-subtle)'
380-
: 'var(--petalytics-subtle)'};"
381-
>{preferences.aiInsights && guardianHelpers.load()?.apiKeyValid ? '☁︎' : ''}</span
382-
>
383-
</div>
384-
385336
<!-- Separator line -->
386337
<div class="my-3">
387338
<div class="border-t" style="border-color: var(--petalytics-border);"></div>
@@ -428,10 +379,6 @@
428379
border-color: var(--petalytics-accent);
429380
box-shadow: 0 0 0 2px color-mix(in oklab, var(--petalytics-accent) 40%, transparent);
430381
}
431-
.cli-row[aria-pressed='true'] {
432-
background: var(--petalytics-highlight-high);
433-
border-color: var(--petalytics-accent);
434-
}
435382
.label {
436383
color: var(--petalytics-foam);
437384
}

src/lib/components/panels/Viewport.svelte

Lines changed: 114 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import RuixenInsights from '../ui/RuixenInsights.svelte';
99
import DataManager from '../ui/DataManager.svelte';
1010
import Button from '../ui/Button.svelte';
11+
import { toast } from '$lib/stores/toast';
1112
import EmptyState from '../ui/EmptyState.svelte';
1213
import Skeleton from '../ui/Skeleton.svelte';
1314
import { rightPanelView, uiHelpers } from '$lib/stores/ui';
@@ -29,7 +30,11 @@
2930
let selectedActivity = '';
3031
let isSubmitting = false;
3132
let loading = false;
32-
let aiEnabled = true;
33+
let apiKeyValid = false;
34+
let runningInsight = false;
35+
let runningWeekly = false;
36+
let weeklyCloudText: string | null = null;
37+
let weeklyForPetId: string | null = null;
3338
3439
// Computed values
3540
$: lastEntry = selectedPet?.journalEntries?.length
@@ -61,9 +66,9 @@
6166
}
6267
6368
onMount(() => {
64-
// Reflect guardian preference for Ruixen insights
69+
// Reflect guardian API key validity for cloud capability
6570
guardianStore.subscribe((g) => {
66-
aiEnabled = !!(g && g.preferences && g.preferences.aiInsights);
71+
apiKeyValid = !!g?.apiKeyValid && !!g?.apiKey;
6772
});
6873
6974
// Subscribe first so incoming loads propagate into state
@@ -92,6 +97,11 @@
9297
selectedPetStore.subscribe((petId) => {
9398
selectedPetId = petId;
9499
selectedPet = petId ? pets.find((p) => p.id === petId) || null : null;
100+
// Clear weekly cloud text if switching pets
101+
if (weeklyForPetId && petId !== weeklyForPetId) {
102+
weeklyCloudText = null;
103+
weeklyForPetId = null;
104+
}
95105
// Keep current view unless user explicitly opened memories.
96106
// Disable actions via disabled buttons when archived is selected.
97107
});
@@ -126,34 +136,7 @@
126136
// Add entry to pet
127137
petHelpers.addJournalEntry(selectedPet.id, entry);
128138
129-
// Analyze with Ruixen orchestrator (rate-limited with offline fallback) if enabled
130-
if (aiEnabled) {
131-
try {
132-
const res = await ruixenHelpers.analyzeDaily(selectedPet, entry);
133-
if (res) {
134-
analysisStore.update((cache) => ({ ...cache, [entry.id]: res }));
135-
// Persist onto the entry so it survives reloads and exports
136-
const petNow = petHelpers.getPet(selectedPet.id);
137-
if (petNow) {
138-
const updatedEntries = (petNow.journalEntries || []).map((e) =>
139-
e.id === entry.id
140-
? {
141-
...e,
142-
aiAnalysis: {
143-
...res,
144-
modelId: (selectedPet as any)?.model || undefined,
145-
analyzedAt: new Date().toISOString(),
146-
},
147-
}
148-
: e
149-
);
150-
petHelpers.update(petNow.id, { journalEntries: updatedEntries });
151-
}
152-
}
153-
} catch (error) {
154-
console.error('Ruixen analysis failed:', error);
155-
}
156-
}
139+
// Skip automatic analysis; allow manual triggers via buttons
157140
158141
// Reset form
159142
journalInput = '';
@@ -169,6 +152,78 @@
169152
isSubmitting = false;
170153
}
171154
}
155+
156+
async function runLastEntryInsight() {
157+
if (!selectedPet || !(selectedPet.journalEntries || []).length) return;
158+
runningInsight = true;
159+
try {
160+
if (!apiKeyValid) {
161+
toast.info('Ruixen is offline', 'Set your API key in Guardian to run cloud analysis');
162+
}
163+
const res = await ruixenHelpers.analyzeLastEntryNow(selectedPet);
164+
const last = (selectedPet.journalEntries || []).slice(-1)[0];
165+
if (res && last) {
166+
analysisStore.update((cache) => ({ ...cache, [last.id]: res }));
167+
const petNow = petHelpers.getPet(selectedPet.id);
168+
if (petNow) {
169+
const updatedEntries = (petNow.journalEntries || []).map((e) =>
170+
e.id === last.id
171+
? {
172+
...e,
173+
aiAnalysis: {
174+
...res,
175+
modelId: (selectedPet as any)?.model || undefined,
176+
analyzedAt: new Date().toISOString(),
177+
},
178+
}
179+
: e
180+
);
181+
petHelpers.update(petNow.id, { journalEntries: updatedEntries });
182+
}
183+
toast.success('Ruixen insight ready', 'Latest entry analyzed');
184+
} else {
185+
toast.info('No entry to analyze');
186+
}
187+
} catch (e) {
188+
const msg = String((e as Error)?.message || e);
189+
if (/429|Rate limit exceeded/i.test(msg)) {
190+
toast.info('Ruixen: Daily free limit reached', 'Try again tomorrow or add credits to OpenRouter');
191+
} else {
192+
toast.error('Insight failed', (e as Error).message);
193+
}
194+
} finally {
195+
runningInsight = false;
196+
}
197+
}
198+
199+
async function runWeeklyCloud() {
200+
if (!selectedPet) return;
201+
runningWeekly = true;
202+
weeklyCloudText = null;
203+
try {
204+
if (!apiKeyValid) {
205+
toast.info('Ruixen is offline', 'Weekly cloud analysis requires an API key');
206+
return;
207+
}
208+
const text = await ruixenHelpers.analyzeWeeklyCloud(selectedPet);
209+
if (text) {
210+
weeklyCloudText = text;
211+
weeklyForPetId = selectedPet.id;
212+
toast.success('Weekly cloud analysis ready');
213+
} else {
214+
toast.warning('Weekly analysis unavailable', 'Please try again later');
215+
}
216+
} catch (e) {
217+
const msg = String((e as Error)?.message || e);
218+
if (/429|Rate limit exceeded/i.test(msg)) {
219+
toast.info('Ruixen: Daily free limit reached', 'Try again tomorrow or add credits to OpenRouter');
220+
} else {
221+
toast.error('Weekly analysis failed', (e as Error).message);
222+
}
223+
} finally {
224+
runningWeekly = false;
225+
}
226+
}
172227
</script>
173228

174229
<div class="viewport-container h-full flex flex-col font-mono">
@@ -505,24 +560,46 @@
505560
<Brain size={16} class="mr-2" style="color: var(--petalytics-accent);" />
506561
Ruixen Insights
507562
</h3>
563+
<div class="flex gap-2 mb-3">
564+
<Button
565+
variant="secondary"
566+
disabled={!selectedPet?.journalEntries?.length || runningInsight || isArchived(selectedPet)}
567+
onclick={runLastEntryInsight}
568+
>
569+
{#if runningInsight}
570+
<div class="animate-spin w-4 h-4 border-2 border-current border-t-transparent rounded-full"></div>
571+
<span>Running…</span>
572+
{:else}
573+
<span>Analyze latest entry</span>
574+
{/if}
575+
</Button>
576+
<Button
577+
variant="secondary"
578+
disabled={!selectedPet || runningWeekly || isArchived(selectedPet)}
579+
onclick={runWeeklyCloud}
580+
>
581+
{#if runningWeekly}
582+
<div class="animate-spin w-4 h-4 border-2 border-current border-t-transparent rounded-full"></div>
583+
<span>Weekly summary…</span>
584+
{:else}
585+
<span>Run 1‑week analysis</span>
586+
{/if}
587+
</Button>
588+
</div>
508589
{#if selectedPet.journalEntries.length === 0}
509590
<p class="text-sm" style="color: var(--petalytics-subtle);">
510591
Add journal entries to get AI-powered insights about {selectedPet.name}'s
511592
well-being.
512593
</p>
513-
{:else if aiEnabled}
514-
<AIInsightsCard petId={selectedPet.id} entryId={lastEntry?.id} compact={true} />
515594
{:else}
516-
<p class="text-xs" style="color: var(--petalytics-subtle);">
517-
Ruixen: offline (enable insights or add API key)
518-
</p>
595+
<AIInsightsCard petId={selectedPet.id} entryId={lastEntry?.id} compact={true} />
519596
{/if}
520597
</div>
521598
</div>
522599

523600
{#if selectedPet}
524601
<div class="mt-4">
525-
<RuixenInsights pet={selectedPet} />
602+
<RuixenInsights pet={selectedPet} cloudWeekly={weeklyCloudText} />
526603
</div>
527604
{/if}
528605
</div>

src/lib/components/ui/AIInsightsCard.svelte

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { Brain, AlertTriangle, CheckCircle, TrendingUp } from 'lucide-svelte';
3-
import { aiAnalysisHelpers } from '$lib/stores/ai-analysis';
3+
import { analysisStore } from '$lib/stores/ai-analysis';
44
import type { AnalysisResult } from '$lib/utils/ai-analysis';
55
66
export let entryId = '';
@@ -9,9 +9,8 @@
99
1010
let analysis: AnalysisResult | null = null;
1111
12-
$: if (entryId) {
13-
analysis = aiAnalysisHelpers.getAnalysis(entryId);
14-
}
12+
// React to analysis store updates and entry changes
13+
$: analysis = entryId ? ($analysisStore[entryId] as AnalysisResult | undefined) || null : null;
1514
</script>
1615

1716
{#if analysis}

0 commit comments

Comments
 (0)