mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-30 13:34:50 +02:00
[eric] memory: one user-curated fact store rides every agent turn, fully visible and editable in Settings
This commit is contained in:
@@ -15,6 +15,7 @@ import AccountCard from './sections/subscription/AccountCard';
|
||||
import GeneralAgentDefaults from './sections/general/GeneralAgentDefaults';
|
||||
import GeneralInterface from './sections/general/GeneralInterface';
|
||||
import DictationSettings from './sections/general/DictationSettings';
|
||||
import MemorySettings from './sections/general/MemorySettings';
|
||||
import CanvasSettings from './sections/general/CanvasSettings';
|
||||
import AgentBehaviorSettings from './sections/general/AgentBehaviorSettings';
|
||||
import GeneralAdvanced from './sections/general/GeneralAdvanced';
|
||||
@@ -32,7 +33,7 @@ import { PROVIDER_COLORS, OPENSWARM_GRADIENT, useModelOptions } from './settings
|
||||
// Module-scope: remember the last open tab across closes (System Settings style).
|
||||
let lastOpenTab: string | null = null;
|
||||
|
||||
const TAB_VALUES = ['account', 'general', 'appearance', 'dictation', 'canvas', 'agents', 'notifications', 'privacy', 'advanced', 'models', 'commands', 'usage'] as const;
|
||||
const TAB_VALUES = ['account', 'general', 'appearance', 'dictation', 'memory', 'canvas', 'agents', 'notifications', 'privacy', 'advanced', 'models', 'commands', 'usage'] as const;
|
||||
type SettingsTab = typeof TAB_VALUES[number];
|
||||
const isValidTab = (t: string | null | undefined): t is SettingsTab =>
|
||||
!!t && (TAB_VALUES as readonly string[]).includes(t);
|
||||
@@ -139,6 +140,10 @@ const SettingsBody: React.FC<SettingsBodyProps> = ({ active, onRequestClose }) =
|
||||
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
|
||||
<DictationSettings form={form} setForm={setForm} styles={styles} />
|
||||
</Box>
|
||||
) : activeTab === 'memory' ? (
|
||||
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
|
||||
<MemorySettings form={form} setForm={setForm} styles={styles} />
|
||||
</Box>
|
||||
) : activeTab === 'canvas' ? (
|
||||
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
|
||||
<CanvasSettings form={form} setForm={setForm} styles={styles} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { User, Settings2, Palette, ShieldCheck, Wrench, Boxes, SquareSlash, BarChart3, Bell, Mic, LayoutGrid, Bot } from 'lucide-react';
|
||||
import { User, Settings2, Palette, ShieldCheck, Wrench, Boxes, SquareSlash, BarChart3, Bell, Mic, LayoutGrid, Bot, Brain } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
@@ -26,6 +26,7 @@ export const RAIL_GROUPS: RailGroup[] = [
|
||||
{ value: 'general', label: 'General', Icon: Settings2 },
|
||||
{ value: 'appearance', label: 'Appearance', Icon: Palette },
|
||||
{ value: 'dictation', label: 'Dictation', Icon: Mic },
|
||||
{ value: 'memory', label: 'Memory', Icon: Brain },
|
||||
{ value: 'canvas', label: 'Canvas', Icon: LayoutGrid },
|
||||
{ value: 'agents', label: 'Agents', Icon: Bot },
|
||||
{ value: 'notifications', label: 'Notifications', Icon: Bell },
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import { AppSettings } from '@/shared/state/settingsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
import type { SettingsStyles } from '../settingsStyles';
|
||||
import { settingSelectAttrs } from '../settingSelect';
|
||||
|
||||
interface MemoryFact {
|
||||
id: string;
|
||||
text: string;
|
||||
source: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** What agents know about you: every fact visible, editable, deletable; nothing hidden. */
|
||||
const MemorySettings: React.FC<{
|
||||
form: AppSettings;
|
||||
setForm: React.Dispatch<React.SetStateAction<AppSettings>>;
|
||||
styles: SettingsStyles;
|
||||
}> = ({ form, setForm, styles }) => {
|
||||
const c = useClaudeTokens();
|
||||
const { inlineRowSx, labelSx, descSx } = styles;
|
||||
const [facts, setFacts] = useState<MemoryFact[]>([]);
|
||||
const [draft, setDraft] = useState('');
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editText, setEditText] = useState('');
|
||||
|
||||
const refresh = async (): Promise<void> => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/memory`);
|
||||
if (res.ok) setFacts(((await res.json()) as { facts: MemoryFact[] }).facts);
|
||||
} catch { /* backend down reads as an empty list, never a crash */ }
|
||||
};
|
||||
useEffect(() => { void refresh(); }, []);
|
||||
|
||||
const add = async (): Promise<void> => {
|
||||
const text = draft.trim();
|
||||
if (!text) return;
|
||||
setDraft('');
|
||||
await fetch(`${API_BASE}/memory`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) });
|
||||
void refresh();
|
||||
};
|
||||
|
||||
const saveEdit = async (): Promise<void> => {
|
||||
if (!editingId) return;
|
||||
const text = editText.trim();
|
||||
setEditingId(null);
|
||||
if (!text) return;
|
||||
await fetch(`${API_BASE}/memory/${editingId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) });
|
||||
void refresh();
|
||||
};
|
||||
|
||||
const remove = async (id: string): Promise<void> => {
|
||||
await fetch(`${API_BASE}/memory/${id}`, { method: 'DELETE' });
|
||||
void refresh();
|
||||
};
|
||||
|
||||
const inputSx = {
|
||||
width: '100%',
|
||||
px: 1.25,
|
||||
py: 0.7,
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${c.border.subtle}`,
|
||||
background: c.bg.surface,
|
||||
color: c.text.primary,
|
||||
fontSize: '0.8438rem',
|
||||
fontFamily: 'inherit',
|
||||
outline: 'none',
|
||||
'&:focus': { borderColor: c.accent.primary },
|
||||
} as const;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={inlineRowSx} {...settingSelectAttrs('memory_enabled', 'Memory', 'Interface', 'Whether agents see your saved facts.')}>
|
||||
<Box sx={{ mr: 3 }}>
|
||||
<Typography sx={labelSx}>Memory</Typography>
|
||||
<Typography sx={descSx}>Agents remember these facts in every chat. Off means none of them reach any model.</Typography>
|
||||
</Box>
|
||||
<Switch size="small" checked={form.memory_enabled !== false} onChange={(e) => setForm({ ...form, memory_enabled: e.target.checked })} />
|
||||
</Box>
|
||||
|
||||
<Box sx={{ ...inlineRowSx, alignItems: 'flex-start', flexDirection: 'column', gap: 1.25 }}>
|
||||
<Box>
|
||||
<Typography sx={labelSx}>What agents know about you</Typography>
|
||||
<Typography sx={descSx}>Add facts yourself, fix wrong ones, delete anything. This list is the whole memory; there is nothing hidden behind it.</Typography>
|
||||
</Box>
|
||||
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 0.25 }}>
|
||||
{facts.length === 0 && (
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.8125rem', py: 0.5 }}>Nothing saved yet.</Typography>
|
||||
)}
|
||||
{facts.map((fact) => (
|
||||
<Box key={fact.id} sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.4, borderBottom: `1px solid ${c.border.subtle}`, '&:last-of-type': { borderBottom: 'none' }, '&:hover .osw-mem-del': { opacity: 1 } }}>
|
||||
{editingId === fact.id ? (
|
||||
<Box
|
||||
component="input"
|
||||
autoFocus
|
||||
value={editText}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEditText(e.target.value)}
|
||||
onBlur={() => void saveEdit()}
|
||||
onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter') void saveEdit(); if (e.key === 'Escape') setEditingId(null); }}
|
||||
sx={inputSx}
|
||||
/>
|
||||
) : (
|
||||
<Typography
|
||||
onClick={() => { setEditingId(fact.id); setEditText(fact.text); }}
|
||||
sx={{ flex: 1, color: c.text.primary, fontSize: '0.8438rem', cursor: 'text', py: 0.3 }}
|
||||
>
|
||||
{fact.text}
|
||||
</Typography>
|
||||
)}
|
||||
{fact.source === 'distilled' && (
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.6875rem', flexShrink: 0 }}>learned</Typography>
|
||||
)}
|
||||
<IconButton className="osw-mem-del" size="small" onClick={() => void remove(fact.id)} sx={{ opacity: 0, transition: 'opacity 0.12s', color: c.text.ghost, '&:hover': { color: c.status.error } }}>
|
||||
<DeleteOutlineIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
<Box
|
||||
component="input"
|
||||
value={draft}
|
||||
placeholder="Add a fact agents should always know (Enter to save)"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setDraft(e.target.value)}
|
||||
onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter') void add(); }}
|
||||
sx={{ ...inputSx, mt: 1 }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemorySettings;
|
||||
@@ -34,6 +34,7 @@ export interface AppSettings {
|
||||
dictation_model?: string | null;
|
||||
dictation_dictionary?: string;
|
||||
dictation_sounds?: boolean;
|
||||
memory_enabled?: boolean;
|
||||
dictation_haptics?: boolean;
|
||||
dictation_sound_volume?: number;
|
||||
dictation_disabled_surfaces?: string;
|
||||
@@ -170,6 +171,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||
dictation_model: null,
|
||||
dictation_dictionary: '',
|
||||
dictation_sounds: true,
|
||||
memory_enabled: true,
|
||||
dictation_haptics: true,
|
||||
dictation_sound_volume: 0.35,
|
||||
dictation_disabled_surfaces: '',
|
||||
|
||||
Reference in New Issue
Block a user