evOo-local/apps/web/src/pages/MemorySettingsOverlay.tsx
cynarAI b2fb517286
Simplify the memory settings: what is remembered first, provider setup under Advanced (#164)
The memory pane opened with an untranslated provider description, a
provider picker, a scope toggle and API key fields, and pushed what the
agents actually remember to the bottom. Most people never connect a
semantic-recall provider, so that setup now sits behind a collapsed
Advanced disclosure (open while a provider is connected), and the
remembered documents come first.

- Drop the provider descriptions, the "Shared documents" heading and
  the per-document revision label.
- Replace the Isolated/Shared toggle pair with one "Share between bots"
  switch.
- Remove the unused standalone dialog mode of the memory pane.
- German: call the section "Gedächtnis" so it no longer reads like
  reminders ("Erinnerungen").


Claude-Session: https://claude.ai/code/session_01LWzKXyE6uDmmmzuod4oXNQ

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-05 20:30:30 +02:00

237 lines
7.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Trans, useLingui } from "@lingui/react/macro";
import type { SpaceMemoryConfig } from "@rakazo/contracts";
import {
Button,
Field,
FieldLabel,
NativeSelect,
NativeSelectOption,
Switch,
} from "@rakazo/ui-web";
import { useEffect, useId, useState } from "react";
import { rpc } from "../lib/rpc";
import { SpaceMemorySection } from "./KnowledgeSection";
import {
defaultMemoryProviderSettings,
MEMORY_PROVIDER_SETTINGS,
type MemoryProviderConnectionDraft,
memoryProviderSettings,
} from "./memory-providers/registry";
type MemoryScope = "isolated" | "shared";
function SharedScopeSwitch({
value,
disabled,
onChange,
}: {
value: MemoryScope;
disabled: boolean;
onChange: (scope: MemoryScope) => void;
}) {
const labelId = useId();
return (
<div className="flex items-center justify-between gap-4">
<span id={labelId} className="text-ui-body text-foreground">
<Trans>Share between bots</Trans>
</span>
<Switch
aria-labelledby={labelId}
checked={value === "shared"}
disabled={disabled}
onCheckedChange={(checked) => onChange(checked ? "shared" : "isolated")}
/>
</div>
);
}
/**
* What the agents remember first; the optional semantic-recall provider stays
* behind Advanced because most people never need it.
*/
export function MemorySettingsOverlay({
config,
onConfigChange,
onBusyChange,
}: {
config: SpaceMemoryConfig | null | undefined;
onConfigChange: (config: SpaceMemoryConfig | null) => void;
onBusyChange?: (busy: boolean) => void;
}) {
const { t } = useLingui();
const providerSelectId = useId();
const defaultRegistration = defaultMemoryProviderSettings();
const [selectedProvider, setSelectedProvider] = useState(
config?.provider ?? defaultRegistration.id,
);
const [defaultScope, setDefaultScope] = useState<MemoryScope>(
config?.defaultMemoryScope ?? "isolated",
);
const [pending, setPending] = useState<"connect" | "disconnect" | "scope" | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (config) {
setSelectedProvider(config.provider);
setDefaultScope(config.defaultMemoryScope);
return;
}
if (config === null && !memoryProviderSettings(selectedProvider)) {
setSelectedProvider(defaultRegistration.id);
}
}, [config, defaultRegistration.id, selectedProvider]);
const registration = memoryProviderSettings(config?.provider ?? selectedProvider);
const busy = pending !== null;
useEffect(() => {
return () => onBusyChange?.(false);
}, [onBusyChange]);
function markPending(next: "connect" | "disconnect" | "scope" | null) {
setPending(next);
onBusyChange?.(next !== null);
}
async function connect(draft: MemoryProviderConnectionDraft) {
if (!registration) return false;
setError(null);
markPending("connect");
try {
const next = await rpc.memory.connectProvider({
provider: registration.id,
...draft,
defaultMemoryScope: defaultScope,
});
onConfigChange(next);
return true;
} catch (err) {
setError(err instanceof Error ? err.message : t`Could not connect ${registration.name}`);
return false;
} finally {
markPending(null);
}
}
async function disconnect() {
setError(null);
markPending("disconnect");
try {
await rpc.memory.disconnectProvider();
onConfigChange(null);
} catch (err) {
setError(err instanceof Error ? err.message : t`Could not disconnect memory provider`);
} finally {
markPending(null);
}
}
async function updateDefaultScope(scope: MemoryScope) {
if (scope === defaultScope) return;
setError(null);
markPending("scope");
try {
const next = await rpc.memory.setDefaultScope({ defaultMemoryScope: scope });
onConfigChange(next);
} catch (err) {
setError(err instanceof Error ? err.message : t`Could not update the default memory scope`);
} finally {
markPending(null);
}
}
return (
<div data-testid="memory-settings" className="flex min-h-0 flex-1 flex-col overflow-hidden">
<div className="rk-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 pb-6 pt-5 sm:px-8 sm:pb-8">
<SpaceMemorySection />
<details data-testid="memory-advanced" className="group mt-8" open={Boolean(config)}>
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 text-ui-body text-muted-foreground">
<span>
<Trans>Advanced</Trans>
</span>
<span aria-hidden="true" className="transition-transform group-open:rotate-90">
›
</span>
</summary>
<div className="mt-4">
{error ? <p className="mb-4 text-sm text-destructive">{error}</p> : null}
{config === undefined ? (
<p className="text-sm text-muted-foreground">
<Trans>Loading memory settings…</Trans>
</p>
) : config ? (
<div className="rounded-xl border border-border px-4 py-3">
<div className="flex items-center justify-between gap-3">
<span className="min-w-0 truncate text-ui-body text-foreground">
{registration?.connectedLabel(config) ?? config.provider}
</span>
<Button
type="button"
variant="outline"
size="sm"
disabled={busy}
onClick={() => void disconnect()}
>
{pending === "disconnect" ? (
<Trans>Disconnecting…</Trans>
) : (
<Trans>Disconnect</Trans>
)}
</Button>
</div>
<div className="mt-3">
<SharedScopeSwitch
value={defaultScope}
disabled={busy}
onChange={(scope) => void updateDefaultScope(scope)}
/>
</div>
</div>
) : registration ? (
<>
{MEMORY_PROVIDER_SETTINGS.length > 1 ? (
<Field className="mb-4">
<FieldLabel htmlFor={providerSelectId}>
<Trans>Provider</Trans>
</FieldLabel>
<NativeSelect
id={providerSelectId}
className="w-full"
value={selectedProvider}
disabled={busy}
onChange={(event) => setSelectedProvider(event.target.value)}
>
{MEMORY_PROVIDER_SETTINGS.map((entry) => (
<NativeSelectOption key={entry.id} value={entry.id}>
{entry.name}
</NativeSelectOption>
))}
</NativeSelect>
</Field>
) : null}
<div className="mb-4">
<SharedScopeSwitch
value={defaultScope}
disabled={busy}
onChange={setDefaultScope}
/>
</div>
<registration.SettingsForm busy={busy} onConnect={connect} />
</>
) : (
<p className="text-sm text-destructive">
<Trans>The selected memory provider is not available in this build.</Trans>
</p>
)}
</div>
</details>
</div>
</div>
);
}