Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
"d3-time-format": "^4.1.0",
"d3-zoom": "^3.0.0",
"date-fns": "^3.6.0",
"express": "^4.18.2",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.0",
"idb": "^8.0.3",
Expand Down
1 change: 0 additions & 1 deletion src/components/dashboard/PersonalizationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,6 @@ export default function PersonalizationPanel() {
style={{
padding: '8px 16px',
borderRadius: '999px',
border: 'none',
background: profile.learningEnabled ? 'var(--green-glow)' : 'var(--bg-elevated)',
color: profile.learningEnabled ? 'var(--green)' : 'var(--text-muted)',
cursor: 'pointer',
Expand Down
1 change: 1 addition & 0 deletions src/components/dashboard/TimeAnalysis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ export default function TimeAnalysis() {
<Tooltip contentStyle={CUSTOM_TOOLTIP} formatter={(v) => [v, 'Operations']} />
<Bar dataKey="count" fill={COLORS.indigo} radius={[2, 2, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</ChartCard>

<DataAggregationOptimizer
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/useConversationNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

import { useReducer, useCallback, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useStore } from '../../lib/store';
import { useStore } from '../lib/store';
import {
classifyNavigationIntent,
type NavIntentType,
type NavigationIntent,
} from '../../lib/navigationClassifier';
} from '../lib/navigationClassifier';
import {
conversationReducer,
initialState,
Expand All @@ -24,7 +24,7 @@ import {
getVoiceGreeting,
getCommandHelpText,
type Message,
} from '../../lib/conversationStore';
} from '../lib/conversationStore';

export function useConversationNavigation() {
const [state, dispatch] = useReducer(conversationReducer, initialState);
Expand Down
14 changes: 14 additions & 0 deletions src/lib/conversationStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ export function getVoiceGreeting(): string {
return '🎤 Listening... Say a navigation command like "show me my portfolio" or "go to network stats".';
}

export function getCommandHelpText(): string {
return [
'**Available commands:**',
'- "Take me to portfolio" → Portfolio overview',
'- "Show network stats" → Network statistics',
'- "Go to transactions" → Transaction history',
'- "Open account viewer" → Account details',
'- "Show DEX analytics" → DEX trading data',
'- "Go to settings" → Settings panel',
'',
"You can also use natural language — I'll do my best to understand your intent.",
].join('\n');
}

// ─── Reducer ──────────────────────────────────────────────────────────────────

export function conversationReducer(
Expand Down
25 changes: 25 additions & 0 deletions src/lib/errorHandling/SelfHealingManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest'
import { selfHealingManager } from './SelfHealingManager'

describe('SelfHealingManager', () => {
it('exposes initial service statuses', () => {
const statuses = selfHealingManager.getStatuses()

expect(statuses.size).toBeGreaterThan(0)
expect(statuses.get('horizon:testnet')?.health).toBe('unknown')
})

it('supports subscriptions and recovery updates', async () => {
const updates: string[] = []
const unsubscribe = selfHealingManager.subscribe(() => {
updates.push('changed')
})

await selfHealingManager.healNow('horizon:testnet')

expect(selfHealingManager.getStatuses().get('horizon:testnet')?.health).toBe('healthy')
expect(updates.length).toBeGreaterThan(0)

unsubscribe()
})
})
197 changes: 197 additions & 0 deletions src/lib/personalizationEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,200 @@ export async function clearPersonalizationData(userId: string): Promise<void> {
resetProfile(userId)
resetSettings(userId)
}

// ─── Profile-based personalization API (used by PersonalizationPanel) ─────────

const PROFILE_STORAGE_KEY = 'stellar_personalization_profile'

export interface PersonalizationProfile {
/** Total number of recorded interactions */
interactionCount: number
/** Map of tab name → visit count */
tabVisits: Record<string, number>
/** Map of widget type → usage count */
widgetUsage: Record<string, number>
/** Dismissed widget suggestions */
dismissedWidgets: string[]
/** Accepted widget suggestions */
acceptedWidgets: string[]
/** Hourly activity counts (index = hour 0–23) */
hourlyActivity: number[]
/** Whether learning mode is active */
learningEnabled: boolean
/** How much transparency to show: full / summary / minimal */
transparencyLevel: 'full' | 'summary' | 'minimal'
/** ISO timestamp of last update */
lastUpdated: string
}

export interface PersonalizationStats {
totalInteractions: number
uniqueTabsVisited: number
uniqueWidgetsUsed: number
suggestionsAccepted: number
estimatedEfficiencyGain: number
topTabs: Array<{ tab: string; count: number }>
topWidgets: Array<{ widget: string; count: number }>
}

export interface WidgetScore {
widgetType: string
score: number
reason: string
}

function defaultProfile(): PersonalizationProfile {
return {
interactionCount: 0,
tabVisits: {},
widgetUsage: {},
dismissedWidgets: [],
acceptedWidgets: [],
hourlyActivity: new Array(24).fill(0),
learningEnabled: true,
transparencyLevel: 'full',
lastUpdated: new Date().toISOString(),
}
}

export async function loadPersonalizationProfile(): Promise<PersonalizationProfile> {
try {
const raw = localStorage.getItem(PROFILE_STORAGE_KEY)
if (raw) return { ...defaultProfile(), ...JSON.parse(raw) }
} catch {
// ignore parse errors
}
return defaultProfile()
}

export async function savePersonalizationProfile(profile: PersonalizationProfile): Promise<void> {
try {
localStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify({ ...profile, lastUpdated: new Date().toISOString() }))
} catch {
// ignore storage errors
}
}

export async function resetPersonalization(): Promise<PersonalizationProfile> {
const fresh = defaultProfile()
await savePersonalizationProfile(fresh)
return fresh
}

export async function recordInteraction(
profile: PersonalizationProfile,
event: { type: string; target: string; metadata?: Record<string, unknown> },
): Promise<PersonalizationProfile> {
if (!profile.learningEnabled) return profile
const updated: PersonalizationProfile = {
...profile,
interactionCount: profile.interactionCount + 1,
tabVisits:
event.type === 'tab_visit'
? { ...profile.tabVisits, [event.target]: (profile.tabVisits[event.target] ?? 0) + 1 }
: profile.tabVisits,
widgetUsage:
event.type === 'widget_use'
? { ...profile.widgetUsage, [event.target]: (profile.widgetUsage[event.target] ?? 0) + 1 }
: profile.widgetUsage,
}
const hour = new Date().getHours()
const hourly = [...updated.hourlyActivity]
hourly[hour] = (hourly[hour] ?? 0) + 1
updated.hourlyActivity = hourly
await savePersonalizationProfile(updated)
return updated
}

export async function recordSuggestionAccepted(
profile: PersonalizationProfile,
widgetType: string,
): Promise<PersonalizationProfile> {
const updated: PersonalizationProfile = {
...profile,
acceptedWidgets: [...new Set([...profile.acceptedWidgets, widgetType])],
dismissedWidgets: profile.dismissedWidgets.filter(w => w !== widgetType),
}
await savePersonalizationProfile(updated)
return updated
}

export async function recordSuggestionDismissed(
profile: PersonalizationProfile,
widgetType: string,
): Promise<PersonalizationProfile> {
const updated: PersonalizationProfile = {
...profile,
dismissedWidgets: [...new Set([...profile.dismissedWidgets, widgetType])],
acceptedWidgets: profile.acceptedWidgets.filter(w => w !== widgetType),
}
await savePersonalizationProfile(updated)
return updated
}

export function computePersonalizationStats(profile: PersonalizationProfile): PersonalizationStats {
const topTabs = Object.entries(profile.tabVisits)
.sort((a, b) => b[1] - a[1])
.map(([tab, count]) => ({ tab, count }))

const topWidgets = Object.entries(profile.widgetUsage)
.sort((a, b) => b[1] - a[1])
.map(([widget, count]) => ({ widget, count }))

const uniqueTabsVisited = Object.keys(profile.tabVisits).length
const uniqueWidgetsUsed = Object.keys(profile.widgetUsage).length

return {
totalInteractions: profile.interactionCount,
uniqueTabsVisited,
uniqueWidgetsUsed,
suggestionsAccepted: profile.acceptedWidgets.length,
estimatedEfficiencyGain: Math.min(95, Math.round(profile.interactionCount / 10)),
topTabs,
topWidgets,
}
}

export function computeWidgetRecommendations(
profile: PersonalizationProfile,
availableTypes: string[],
_currentWidgets: string[],
): WidgetScore[] {
return availableTypes
.filter(type => !profile.dismissedWidgets.includes(type))
.map(type => {
const usage = profile.widgetUsage[type] ?? 0
const accepted = profile.acceptedWidgets.includes(type)
const score = Math.min(100, (usage * 10) + (accepted ? 30 : 0) + Math.random() * 20)
const reason =
usage > 5
? `You've used this widget ${usage} times — it fits your workflow well.`
: accepted
? 'Previously added to your layout.'
: 'Might complement your existing setup.'
return { widgetType: type, score, reason }
})
.sort((a, b) => b.score - a.score)
}

export function identifyPeakUsageHours(profile: PersonalizationProfile): number[] {
return profile.hourlyActivity
.map((count, hour) => ({ hour, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 3)
.filter(e => e.count > 0)
.map(e => e.hour)
}

export function detectPowerUser(profile: PersonalizationProfile): boolean {
return profile.interactionCount > 100 || Object.keys(profile.tabVisits).length > 6
}

export function detectCasualUser(profile: PersonalizationProfile): boolean {
return profile.interactionCount < 20
}

export function computeLayoutCompactnessScore(profile: PersonalizationProfile): number {
const widgetCount = Object.keys(profile.widgetUsage).length
return Math.min(1, widgetCount / 8)
}
Loading
Loading