Initial commit: NexusOS project + desktop theme baseline
Captures the known-good state after theme consolidation and consistency reconciliation: - NexusOS GTK/xfwm4/icon theme assets (assets/themes, management/Mint-Y-Nexus) - Tightened right-click menus, visible separators, no menu icons - assets/themes/install-theme.sh: idempotent restore of all wiring (symlinks, xfconf xsettings+xfwm4, GTK 3/4 settings.ini) - .gitignore excludes venv/ollama/models/runtime/db Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { API_BASE } from "./config";
|
||||
|
||||
const DEFAULT_SECTIONS = ["Instructions", "Identity", "Career", "Projects", "Preferences"];
|
||||
|
||||
function groupBySection(items) {
|
||||
const map = {};
|
||||
for (const item of items) {
|
||||
const sec = item.section || "General";
|
||||
if (!map[sec]) map[sec] = [];
|
||||
map[sec].push(item);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
export function Memory() {
|
||||
const [items, setItems] = useState([]);
|
||||
const [editing, setEditing] = useState(null); // { id, section, text } or null
|
||||
const [adding, setAdding] = useState(null); // { section } for inline new-item form
|
||||
const [newSection, setNewSection] = useState("");
|
||||
const [showNewSection, setShowNewSection] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const loadItems = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/memory`);
|
||||
const data = res.ok ? await res.json() : { items: [] };
|
||||
setItems(data.items || []);
|
||||
} catch {
|
||||
setItems([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { loadItems(); }, [loadItems]);
|
||||
|
||||
const flash = (msg) => { setMessage(msg); setTimeout(() => setMessage(""), 3000); };
|
||||
|
||||
const saveNew = async (section, text) => {
|
||||
if (!text.trim()) return;
|
||||
const res = await fetch(`${API_BASE}/memory`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ section, text: text.trim() }),
|
||||
});
|
||||
if (res.ok) { loadItems(); setAdding(null); flash("Saved."); }
|
||||
else flash("Save failed.");
|
||||
};
|
||||
|
||||
const saveEdit = async (id, section, text) => {
|
||||
if (!text.trim()) return;
|
||||
const res = await fetch(`${API_BASE}/memory/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ section, text: text.trim() }),
|
||||
});
|
||||
if (res.ok) { loadItems(); setEditing(null); flash("Updated."); }
|
||||
else flash("Update failed.");
|
||||
};
|
||||
|
||||
const deleteItem = async (id) => {
|
||||
if (!window.confirm("Delete this item?")) return;
|
||||
const res = await fetch(`${API_BASE}/memory/${id}`, { method: "DELETE" });
|
||||
if (res.ok) { loadItems(); flash("Deleted."); }
|
||||
else flash("Delete failed.");
|
||||
};
|
||||
|
||||
const addSection = () => {
|
||||
const sec = newSection.trim();
|
||||
if (!sec) return;
|
||||
// Create a placeholder item to establish the section, then let user fill it
|
||||
setAdding({ section: sec });
|
||||
setNewSection("");
|
||||
setShowNewSection(false);
|
||||
};
|
||||
|
||||
const grouped = groupBySection(items);
|
||||
const sections = Object.keys(grouped);
|
||||
|
||||
const inputStyle = {
|
||||
background: "#1a1a1a",
|
||||
color: "#eee",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "6px",
|
||||
padding: "0.4rem 0.6rem",
|
||||
fontSize: "0.9rem",
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
};
|
||||
|
||||
const btnStyle = (color = "#007acc") => ({
|
||||
padding: "0.3rem 0.7rem",
|
||||
background: color,
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.8rem",
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ flexGrow: 1, maxWidth: "860px" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "1.25rem" }}>
|
||||
<div>
|
||||
<h2 style={{ margin: 0, fontSize: "1.1rem" }}>Memory</h2>
|
||||
<p style={{ margin: "0.25rem 0 0", color: "#888", fontSize: "0.85rem" }}>
|
||||
Facts injected into every conversation. Format mirrors your claude.md.
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => setShowNewSection(s => !s)} style={btnStyle()}>
|
||||
+ New Section
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showNewSection && (
|
||||
<div style={{ display: "flex", gap: "0.5rem", marginBottom: "1.25rem" }}>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Section name (e.g. Health, Finance)"
|
||||
value={newSection}
|
||||
onChange={e => setNewSection(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === "Enter") addSection(); if (e.key === "Escape") setShowNewSection(false); }}
|
||||
style={{ ...inputStyle, maxWidth: "320px" }}
|
||||
/>
|
||||
<button onClick={addSection} style={btnStyle()}>Add</button>
|
||||
<button onClick={() => setShowNewSection(false)} style={btnStyle("#444")}>Cancel</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sections.length === 0 && !adding && (
|
||||
<div style={{ color: "#666", fontSize: "0.9rem", marginTop: "2rem" }}>
|
||||
No memory yet. Add a section to get started.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Render sections in claude.md style */}
|
||||
{[...sections, ...(adding && !sections.includes(adding.section) ? [adding.section] : [])].map(section => (
|
||||
<div key={section} style={{ marginBottom: "2rem" }}>
|
||||
{/* ## Section Header */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", marginBottom: "0.5rem" }}>
|
||||
<h3 style={{ margin: 0, fontSize: "1rem", color: "#4db8ff" }}>## {section}</h3>
|
||||
<button
|
||||
onClick={() => setAdding({ section })}
|
||||
style={{ ...btnStyle("#2a3a2a"), border: "1px solid #3a5a3a", color: "#7dcc7d", fontSize: "0.75rem" }}
|
||||
>
|
||||
+ Add item
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Bullet items */}
|
||||
<div style={{ paddingLeft: "1rem", borderLeft: "2px solid #222" }}>
|
||||
{(grouped[section] || []).map(item => (
|
||||
<div key={item.id} style={{ marginBottom: "0.4rem" }}>
|
||||
{editing?.id === item.id ? (
|
||||
<EditRow
|
||||
initial={editing.text}
|
||||
onSave={text => saveEdit(item.id, section, text)}
|
||||
onCancel={() => setEditing(null)}
|
||||
inputStyle={inputStyle}
|
||||
btnStyle={btnStyle}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ display: "flex", alignItems: "flex-start", gap: "0.5rem", group: true }}>
|
||||
<span style={{ color: "#aaa", marginTop: "0.1rem", flexShrink: 0 }}>-</span>
|
||||
<span style={{ color: "#ddd", fontSize: "0.9rem", flexGrow: 1 }}>{item.text}</span>
|
||||
<div style={{ display: "flex", gap: "0.3rem", flexShrink: 0, opacity: 0.4 }}
|
||||
onMouseEnter={e => e.currentTarget.style.opacity = 1}
|
||||
onMouseLeave={e => e.currentTarget.style.opacity = 0.4}
|
||||
>
|
||||
<button
|
||||
onClick={() => setEditing({ id: item.id, section, text: item.text })}
|
||||
style={{ ...btnStyle("#333"), fontSize: "0.72rem", padding: "0.2rem 0.5rem" }}
|
||||
>edit</button>
|
||||
<button
|
||||
onClick={() => deleteItem(item.id)}
|
||||
style={{ ...btnStyle("#5a1a1a"), fontSize: "0.72rem", padding: "0.2rem 0.5rem" }}
|
||||
>del</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Inline add form */}
|
||||
{adding?.section === section && (
|
||||
<AddRow
|
||||
onSave={text => saveNew(section, text)}
|
||||
onCancel={() => setAdding(null)}
|
||||
inputStyle={inputStyle}
|
||||
btnStyle={btnStyle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{message && (
|
||||
<div style={{ position: "fixed", bottom: "1.5rem", right: "1.5rem", background: "#1a2e1a", border: "1px solid #3a5a3a", color: "#7dcc7d", padding: "0.6rem 1rem", borderRadius: "8px", fontSize: "0.85rem" }}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AddRow({ onSave, onCancel, inputStyle, btnStyle }) {
|
||||
const [text, setText] = useState("");
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "0.5rem", alignItems: "center", marginTop: "0.4rem" }}>
|
||||
<span style={{ color: "#555", flexShrink: 0 }}>-</span>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="New item..."
|
||||
value={text}
|
||||
onChange={e => setText(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === "Enter") onSave(text); if (e.key === "Escape") onCancel(); }}
|
||||
style={{ ...inputStyle }}
|
||||
/>
|
||||
<button onClick={() => onSave(text)} style={btnStyle()}>Save</button>
|
||||
<button onClick={onCancel} style={btnStyle("#444")}>✕</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EditRow({ initial, onSave, onCancel, inputStyle, btnStyle }) {
|
||||
const [text, setText] = useState(initial);
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}>
|
||||
<span style={{ color: "#555", flexShrink: 0 }}>-</span>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
value={text}
|
||||
onChange={e => setText(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === "Enter") onSave(text); if (e.key === "Escape") onCancel(); }}
|
||||
style={{ ...inputStyle }}
|
||||
/>
|
||||
<button onClick={() => onSave(text)} style={btnStyle()}>Save</button>
|
||||
<button onClick={onCancel} style={btnStyle("#444")}>✕</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user