f721d87423
- Skills: categorized list with search, detail view with markdown - Cron: job list grouped by status, manual trigger button - Soul: full SOUL.md editor with save/revert - Pulse: quick link cards to Skills, Cron, Soul - All routes proxied server-side (no CORS)
85 lines
2.8 KiB
TypeScript
85 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { Sparkles, Save, RotateCcw } from "lucide-react";
|
|
|
|
export default function SoulPage() {
|
|
const [content, setContent] = useState("");
|
|
const [original, setOriginal] = useState("");
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [saved, setSaved] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetch("/soul/api")
|
|
.then((r) => r.json())
|
|
.then((d) => { setContent(d.content || ""); setOriginal(d.content || ""); })
|
|
.catch(() => {})
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
const dirty = content !== original;
|
|
|
|
const save = async () => {
|
|
setSaving(true);
|
|
await fetch("/soul/api", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ content }),
|
|
});
|
|
setOriginal(content);
|
|
setSaving(false);
|
|
setSaved(true);
|
|
setTimeout(() => setSaved(false), 2000);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Sparkles size={20} style={{ color: "#ec4899" }} />
|
|
<h1 className="text-xl font-bold" style={{ color: "#ec4899" }}>SOUL.md</h1>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{dirty && (
|
|
<button
|
|
onClick={() => setContent(original)}
|
|
className="p-2 rounded-xl active:scale-95 text-zinc-500"
|
|
>
|
|
<RotateCcw size={16} />
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={save}
|
|
disabled={!dirty || saving}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-medium transition-all disabled:opacity-30"
|
|
style={{ background: "rgba(236,72,153,0.15)", color: "#ec4899" }}
|
|
>
|
|
<Save size={14} />
|
|
{saving ? "Saving..." : saved ? "Saved!" : "Save"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-xs text-zinc-500">
|
|
Persona file loaded fresh each message. Edit to change how Hermes communicates.
|
|
</p>
|
|
|
|
{loading ? (
|
|
<div className="glass p-4 animate-pulse space-y-2">
|
|
<div className="h-4 bg-white/[0.06] rounded w-full" />
|
|
<div className="h-4 bg-white/[0.06] rounded w-3/4" />
|
|
<div className="h-4 bg-white/[0.06] rounded w-1/2" />
|
|
</div>
|
|
) : (
|
|
<textarea
|
|
value={content}
|
|
onChange={(e) => setContent(e.target.value)}
|
|
className="w-full min-h-[60vh] p-4 rounded-2xl text-sm text-zinc-300 bg-white/[0.04] border border-white/[0.08] outline-none focus:border-white/[0.15] resize-none font-mono leading-relaxed transition-colors"
|
|
spellCheck={false}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|