spool-memory 0.1.0

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
import { useState, useEffect } from 'react'
import { invoke as tauriInvoke } from '@tauri-apps/api/core'
import { isTauriHost } from '@/lib/api/desktop'
import { Input } from '@/components/ui/input'
import { Plus, Trash2 } from 'lucide-react'

type ExtractionRule = { trigger: string; memory_type: string; description: string }
type ContextRule = { scope: string; always_include: string[]; description: string }
type SuppressRule = { pattern: string; action: string; description: string }
type Rules = { extraction: ExtractionRule[]; context: ContextRule[]; suppress: SuppressRule[] }

export function RulesEditor() {
  const [rules, setRules] = useState<Rules>({ extraction: [], context: [], suppress: [] })
  const [saving, setSaving] = useState(false)
  const [msg, setMsg] = useState('')

  useEffect(() => {
    if (!isTauriHost()) return
    tauriInvoke<Rules>('desktop_get_rules').then(setRules).catch(() => {})
  }, [])

  async function handleSave() {
    if (!isTauriHost()) return
    setSaving(true)
    setMsg('')
    try {
      await tauriInvoke('desktop_save_rules', { rules })
      setMsg('已保存')
    } catch (e) {
      setMsg(`保存失败: ${e}`)
    }
    setSaving(false)
  }

  function addExtraction() {
    setRules((prev) => ({
      ...prev,
      extraction: [...prev.extraction, { trigger: '', memory_type: 'preference', description: '' }],
    }))
  }

  function removeExtraction(idx: number) {
    setRules((prev) => ({
      ...prev,
      extraction: prev.extraction.filter((_, i) => i !== idx),
    }))
  }

  function updateExtraction(idx: number, field: keyof ExtractionRule, value: string) {
    setRules((prev) => ({
      ...prev,
      extraction: prev.extraction.map((r, i) => (i === idx ? { ...r, [field]: value } : r)),
    }))
  }

  function addSuppress() {
    setRules((prev) => ({
      ...prev,
      suppress: [...prev.suppress, { pattern: '', action: 'skip', description: '' }],
    }))
  }

  function removeSuppress(idx: number) {
    setRules((prev) => ({
      ...prev,
      suppress: prev.suppress.filter((_, i) => i !== idx),
    }))
  }

  function updateSuppress(idx: number, field: keyof SuppressRule, value: string) {
    setRules((prev) => ({
      ...prev,
      suppress: prev.suppress.map((r, i) => (i === idx ? { ...r, [field]: value } : r)),
    }))
  }

  return (
    <div className="space-y-4">
      {/* 提炼规则 */}
      <div>
        <div className="flex items-center justify-between mb-2">
          <h4 className="text-xs font-medium">提炼触发词</h4>
          <button onClick={addExtraction} className="flex items-center gap-1 rounded border px-2 py-0.5 text-[10px] hover:bg-muted transition-colors">
            <Plus className="h-3 w-3" /> 添加
          </button>
        </div>
        <p className="text-[10px] text-muted-foreground mb-2">
          当对话中出现这些触发词时,自动提炼为对应类型的记忆
        </p>
        {rules.extraction.map((rule, idx) => (
          <div key={idx} className="flex items-center gap-2 mb-1.5">
            <Input
              value={rule.trigger}
              onChange={(e) => updateExtraction(idx, 'trigger', e.target.value)}
              placeholder="触发词"
              className="text-xs h-7 flex-1"
            />
            <select
              value={rule.memory_type}
              onChange={(e) => updateExtraction(idx, 'memory_type', e.target.value)}
              className="h-7 rounded border bg-background px-2 text-xs"
            >
              <option value="preference">偏好</option>
              <option value="decision">决策</option>
              <option value="constraint">约束</option>
              <option value="workflow">工作流</option>
              <option value="incident">事件</option>
            </select>
            <button onClick={() => removeExtraction(idx)} className="p-1 text-muted-foreground hover:text-destructive">
              <Trash2 className="h-3 w-3" />
            </button>
          </div>
        ))}
        {rules.extraction.length === 0 && (
          <p className="text-[10px] text-muted-foreground italic">暂无规则,点击添加</p>
        )}
      </div>

      {/* 抑制规则 */}
      <div>
        <div className="flex items-center justify-between mb-2">
          <h4 className="text-xs font-medium">抑制规则</h4>
          <button onClick={addSuppress} className="flex items-center gap-1 rounded border px-2 py-0.5 text-[10px] hover:bg-muted transition-colors">
            <Plus className="h-3 w-3" /> 添加
          </button>
        </div>
        <p className="text-[10px] text-muted-foreground mb-2">
          匹配这些模式的内容不会被提炼为记忆
        </p>
        {rules.suppress.map((rule, idx) => (
          <div key={idx} className="flex items-center gap-2 mb-1.5">
            <Input
              value={rule.pattern}
              onChange={(e) => updateSuppress(idx, 'pattern', e.target.value)}
              placeholder="正则模式"
              className="text-xs h-7 flex-1 font-mono"
            />
            <button onClick={() => removeSuppress(idx)} className="p-1 text-muted-foreground hover:text-destructive">
              <Trash2 className="h-3 w-3" />
            </button>
          </div>
        ))}
        {rules.suppress.length === 0 && (
          <p className="text-[10px] text-muted-foreground italic">暂无规则,点击添加</p>
        )}
      </div>

      {/* 保存 */}
      <div className="flex items-center gap-2 pt-2">
        <button
          onClick={handleSave}
          disabled={saving}
          className="rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50"
        >
          {saving ? '保存中...' : '保存规则'}
        </button>
        {msg && <span className="text-[10px] text-muted-foreground">{msg}</span>}
      </div>
    </div>
  )
}