File size: 9,741 Bytes
f667d47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Sliders, RotateCcw, Save, CheckCircle2, Users, AlertTriangle } from 'lucide-react'
import { cn } from '@/lib/utils'
import { wallets } from '@/lib/mock-data'
import { calculateChurnScore, DEFAULT_THRESHOLDS, ScoringThresholds } from '@/lib/agent-engine'
import type { ChurnRisk } from '@/lib/types'

const STORAGE_KEY = 'flowstate_thresholds'

function loadThresholds(): ScoringThresholds {
  if (typeof window === 'undefined') return DEFAULT_THRESHOLDS
  try {
    const raw = localStorage.getItem(STORAGE_KEY)
    return raw ? { ...DEFAULT_THRESHOLDS, ...JSON.parse(raw) } : DEFAULT_THRESHOLDS
  } catch {
    return DEFAULT_THRESHOLDS
  }
}

function walletToSignals(w: typeof wallets[0]) {
  const daysMatch = w.lastActive.match(/(\d+)d/)
  const daysInactive = daysMatch ? parseInt(daysMatch[1]) : 0
  return {
    daysInactive,
    volumeDropPct: w.streak === 0 ? 80 : Math.max(0, (10 - Math.min(w.streak, 10)) * 8),
    uniqueProtocols: w.protocols.length,
    currentStreak: w.streak,
    hasLiquidation: false,
  }
}

const RISK_COLORS: Record<ChurnRisk, string> = {
  critical: 'bg-trading-down/20 text-trading-down',
  high: 'bg-[#ff9500]/20 text-[#ff9500]',
  medium: 'bg-brand-yellow/20 text-brand-yellow',
  low: 'bg-trading-up/20 text-trading-up',
  safe: 'bg-muted/20 text-muted',
}

interface SliderRowProps {
  label: string; desc: string; value: number; min: number; max: number; unit: string
  onChange: (v: number) => void
}
function SliderRow({ label, desc, value, min, max, unit, onChange }: SliderRowProps) {
  return (
    <div className="space-y-2">
      <div className="flex items-center justify-between">
        <div>
          <p className="text-body-sm font-medium text-[#eaecef]">{label}</p>
          <p className="text-caption text-muted">{desc}</p>
        </div>
        <span className="font-mono text-num-sm text-brand-yellow tabular-nums w-16 text-right">{value}{unit}</span>
      </div>
      <input
        type="range" min={min} max={max} value={value}
        onChange={e => onChange(parseInt(e.target.value))}
        className="w-full h-1.5 rounded-full appearance-none bg-surface-elevated cursor-pointer accent-brand-yellow"
      />
      <div className="flex justify-between text-[10px] text-muted">
        <span>{min}{unit}</span><span>{max}{unit}</span>
      </div>
    </div>
  )
}

export default function SettingsPage() {
  const [thresholds, setThresholds] = useState<ScoringThresholds>(DEFAULT_THRESHOLDS)
  const [saved, setSaved] = useState(false)

  useEffect(() => { setThresholds(loadThresholds()) }, [])

  const set = useCallback(<K extends keyof ScoringThresholds>(key: K, value: number) => {
    setThresholds(t => ({ ...t, [key]: value }))
  }, [])

  const preview = wallets.map(w => {
    const signals = walletToSignals(w)
    const { score, risk } = calculateChurnScore(signals, thresholds)
    return { ...w, score, risk }
  })

  const riskCounts = preview.reduce<Record<string, number>>((acc, w) => {
    acc[w.risk] = (acc[w.risk] || 0) + 1
    return acc
  }, {})

  const handleSave = () => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(thresholds))
    setSaved(true)
    setTimeout(() => setSaved(false), 2000)
  }

  const handleReset = () => {
    setThresholds(DEFAULT_THRESHOLDS)
    localStorage.removeItem(STORAGE_KEY)
  }

  return (
    <div className="p-6 space-y-6 max-w-4xl">
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-display-sm text-[#eaecef]">Threshold Editor</h1>
          <p className="text-body-md text-muted mt-1">Tune the 5-signal AI scoring model β€” live preview updates wallet risk tiers</p>
        </div>
        <div className="flex items-center gap-2">
          <button onClick={handleReset} className="flex items-center gap-2 px-3 py-2 rounded-md border border-hairline-dark text-muted hover:text-[#eaecef] text-button transition">
            <RotateCcw className="w-3.5 h-3.5" />Reset
          </button>
          <button onClick={handleSave} className="flex items-center gap-2 px-4 py-2 rounded-md bg-brand-yellow text-ink text-button font-semibold hover:bg-brand-yellow-active transition">
            {saved ? <CheckCircle2 className="w-3.5 h-3.5" /> : <Save className="w-3.5 h-3.5" />}
            {saved ? 'Saved!' : 'Save Thresholds'}
          </button>
        </div>
      </div>

      {/* Live preview */}
      <div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
        <div className="flex items-center gap-2 mb-4">
          <Users className="w-4 h-4 text-brand-yellow" />
          <h3 className="text-title-sm">Live Preview β€” Wallet Distribution</h3>
          <span className="text-caption text-muted ml-1">updates as you drag</span>
        </div>
        <div className="flex gap-3 flex-wrap">
          {(['critical','high','medium','low','safe'] as ChurnRisk[]).map(r => (
            <div key={r} className={cn('flex-1 min-w-[80px] rounded-xl border p-4 text-center', r === 'critical' ? 'border-trading-down/30' : r === 'high' ? 'border-[#ff9500]/30' : r === 'medium' ? 'border-brand-yellow/30' : r === 'low' ? 'border-trading-up/30' : 'border-hairline-dark')}>
              <p className="text-caption text-muted capitalize mb-1">{r}</p>
              <p className="font-mono text-title-lg text-[#eaecef] tabular-nums">{riskCounts[r] || 0}</p>
            </div>
          ))}
        </div>
        <div className="mt-4 space-y-1">
          {preview.map(w => (
            <div key={w.address} className="flex items-center gap-3 py-1.5 px-3 rounded-lg hover:bg-surface-elevated/50">
              <span className="font-mono text-caption text-muted w-36 truncate">{w.address.slice(0,8)}...{w.address.slice(-4)}</span>
              <span className={cn('text-[10px] font-semibold uppercase px-2 py-0.5 rounded-pill', RISK_COLORS[w.risk as ChurnRisk])}>{w.risk}</span>
              <div className="flex-1 h-1 rounded-full bg-surface-elevated overflow-hidden">
                <div className="h-full rounded-full bg-brand-yellow/60 transition-all duration-300" style={{width: w.score + '%'}} />
              </div>
              <span className="font-mono text-num-sm text-muted tabular-nums w-8">{w.score}</span>
            </div>
          ))}
        </div>
      </div>

      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
        {/* Inactivity thresholds */}
        <div className="rounded-xl bg-surface-card border border-hairline-dark p-5 space-y-6">
          <div className="flex items-center gap-2">
            <AlertTriangle className="w-4 h-4 text-trading-down" />
            <h3 className="text-title-sm">Inactivity Signals</h3>
          </div>
          <SliderRow label="Critical threshold" desc="Days inactive β†’ critical risk signal" value={thresholds.inactivityCritical} min={7} max={60} unit="d" onChange={v => set('inactivityCritical', v)} />
          <SliderRow label="High threshold" desc="Days inactive β†’ high risk signal" value={thresholds.inactivityHigh} min={3} max={30} unit="d" onChange={v => set('inactivityHigh', v)} />
          <SliderRow label="Medium threshold" desc="Days inactive β†’ medium risk signal" value={thresholds.inactivityMedium} min={1} max={14} unit="d" onChange={v => set('inactivityMedium', v)} />
        </div>

        {/* Volume drop thresholds */}
        <div className="rounded-xl bg-surface-card border border-hairline-dark p-5 space-y-6">
          <div className="flex items-center gap-2">
            <Sliders className="w-4 h-4 text-brand-yellow" />
            <h3 className="text-title-sm">Volume Drop Signals</h3>
          </div>
          <SliderRow label="Critical drop" desc="Volume decline % β†’ critical signal" value={thresholds.volumeDropCritical} min={50} max={100} unit="%" onChange={v => set('volumeDropCritical', v)} />
          <SliderRow label="High drop" desc="Volume decline % β†’ high signal" value={thresholds.volumeDropHigh} min={25} max={90} unit="%" onChange={v => set('volumeDropHigh', v)} />
          <SliderRow label="Medium drop" desc="Volume decline % β†’ medium signal" value={thresholds.volumeDropMedium} min={10} max={60} unit="%" onChange={v => set('volumeDropMedium', v)} />
        </div>

        {/* Pre-churn early warning */}
        <div className="rounded-xl bg-surface-card border border-hairline-dark p-5 space-y-6 lg:col-span-2">
          <div>
            <div className="flex items-center gap-2">
              <span className="text-sm">⚑</span>
              <h3 className="text-title-sm">Pre-Churn Early Warning</h3>
              <span className="text-[10px] px-2 py-0.5 rounded-pill bg-trading-up/10 text-trading-up font-semibold border border-trading-up/20">3Γ— more effective than win-back</span>
            </div>
            <p className="text-caption text-muted mt-1">Fire <code className="text-brand-yellow/80">inactivity_detected</code> before wallets reach churn threshold. Different sensitivity per wallet type.</p>
          </div>
          <div className="grid grid-cols-3 gap-6">
            <SliderRow label="Trader" desc="High-frequency DEX users" value={thresholds.preChurnDaysTrader} min={1} max={7} unit="d" onChange={v => set('preChurnDaysTrader', v)} />
            <SliderRow label="LP / Lender" desc="Kamino, Marginfi users" value={thresholds.preChurnDaysLP} min={2} max={10} unit="d" onChange={v => set('preChurnDaysLP', v)} />
            <SliderRow label="Staker" desc="Low-frequency holders" value={thresholds.preChurnDaysStaker} min={3} max={14} unit="d" onChange={v => set('preChurnDaysStaker', v)} />
          </div>
        </div>
      </div>
    </div>
  )
}