Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 47 additions & 49 deletions dashboard/src/v2/MemoryPage.tsx

Large diffs are not rendered by default.

30 changes: 16 additions & 14 deletions dashboard/src/v2/components/memory/AddMemoryModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { MemoryScope, MemoryCategory } from "../../memory-types.js";
import { FieldWrapper } from "../forms/FieldWrapper.js";
import { CheckCircle2, Loader2 } from "lucide-preact";
import { useInteractionTokens } from "../../lib/motion/index.js";
import { MEMORY_CATEGORY_MESSAGE_KEYS, useMemoryI18n } from "../../i18n/messages/memory.js";

const CATEGORIES: MemoryCategory[] = ["architecture", "codebase", "context", "preferences", "patterns", "decision", "error", "learning"];

Expand All @@ -25,6 +26,7 @@ export const AddMemoryModal: FunctionComponent<{
const openerRef = useRef<HTMLElement | null>(null);
const closeTimerRef = useRef<number | null>(null);
const interactionTokens = useInteractionTokens();
const { formatNumber, t } = useMemoryI18n();

useEffect(() => {
if (!open) {
Expand Down Expand Up @@ -65,7 +67,7 @@ export const AddMemoryModal: FunctionComponent<{
event.preventDefault();
if (!content.trim()) {
setShowError(true);
setFeedback({ status: "error", message: "Add a memory description before submitting." });
setFeedback({ status: "error", message: t("addDescriptionBeforeSubmit") });
contentRef.current?.focus({ preventScroll: true });
return;
}
Expand All @@ -75,7 +77,7 @@ export const AddMemoryModal: FunctionComponent<{
await createMemory(projectId, { scope, content: content.trim(), category, strength });
setContent("");
setShowError(false);
setFeedback({ status: "success", message: "Memory added. Refreshing the workspace." });
setFeedback({ status: "success", message: t("memoryAddedRefreshing") });
await onCreated();
closeTimerRef.current = window.setTimeout(() => {
closeTimerRef.current = null;
Expand All @@ -85,7 +87,7 @@ export const AddMemoryModal: FunctionComponent<{
} catch (error) {
setFeedback({
status: "error",
message: error instanceof Error ? error.message : "Failed to add memory. Check the content and try again."
message: error instanceof Error ? error.message : t("addMemoryFallbackError")
});
} finally {
setSaving(false);
Expand All @@ -112,8 +114,8 @@ export const AddMemoryModal: FunctionComponent<{
onClick={e => e.stopPropagation()}
onSubmit={(event) => { void handleSubmit(event); }}
role="dialog" aria-modal="true" aria-labelledby="add-memory-title" aria-describedby="add-memory-status" aria-busy={saving}>
<h3 id="add-memory-title" className="text-base font-semibold text-slate-900 dark:text-white font-display">Add Memory</h3>
<FieldWrapper label="Memory Content" htmlFor="memory-content" required forceTouch={showError} error={contentInvalid ? "Content is required" : undefined}>
<h3 id="add-memory-title" className="text-base font-semibold text-slate-900 dark:text-white font-display">{t("addMemoryTitle")}</h3>
<FieldWrapper label={t("memoryContent")} htmlFor="memory-content" required forceTouch={showError} error={contentInvalid ? t("contentRequired") : undefined}>
<textarea id="memory-content" ref={contentRef} value={content}
aria-invalid={contentInvalid}
aria-describedby={describedBy}
Expand All @@ -124,7 +126,7 @@ export const AddMemoryModal: FunctionComponent<{
setFeedback({ status: "idle", message: null });
}
}}
placeholder="What should be remembered…"
placeholder={t("rememberPlaceholder")}
rows={3}
className="w-full px-4 py-3 rounded-xl text-sm
bg-black/[0.03] dark:bg-white/[0.03]
Expand All @@ -136,21 +138,21 @@ export const AddMemoryModal: FunctionComponent<{
</FieldWrapper>
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 w-full">
<div className="w-full sm:flex-1">
<FieldWrapper label="Category" htmlFor="memory-category">
<FieldWrapper label={t("category")} htmlFor="memory-category">
<select id="memory-category" value={category} onChange={e => setCategory((e.target as HTMLSelectElement).value as MemoryCategory)}
className="w-full px-3 py-2 rounded-lg text-xs font-medium cursor-pointer
bg-black/[0.03] dark:bg-white/[0.03] hover:bg-black/[0.05] dark:hover:bg-white/[0.05] transition-colors duration-200
border border-black/[0.06] dark:border-white/[0.06]
text-slate-700 dark:text-slate-300
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-signal-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-void-800">
{CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
{CATEGORIES.map(c => <option key={c} value={c}>{t(MEMORY_CATEGORY_MESSAGE_KEYS[c])}</option>)}
</select>
</FieldWrapper>
</div>
<div className="w-full sm:w-auto flex items-center gap-2">
<FieldWrapper label="Strength" htmlFor="memory-strength">
<FieldWrapper label={t("strength")} htmlFor="memory-strength">
<div className="flex items-center gap-2 mt-1">
<span className="text-[10px] font-mono text-slate-400">{Math.round(strength * 100)}%</span>
<span className="text-[10px] font-mono text-slate-400">{formatNumber(strength, { style: "percent", maximumFractionDigits: 0 })}</span>
<input type="range" id="memory-strength" min="0.1" max="1" step="0.1" value={strength}
onInput={e => setStrength(parseFloat((e.target as HTMLInputElement).value))}
className="w-20 accent-signal-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-signal-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-void-800 cursor-pointer" />
Expand All @@ -175,18 +177,18 @@ export const AddMemoryModal: FunctionComponent<{
style={{ transitionDuration: interactionTokens.asyncFeedback.duration, transitionTimingFunction: interactionTokens.asyncFeedback.ease }}
>
{saving ? (
<span className="inline-flex items-center gap-2"><Loader2 size={14} className="motion-safe:animate-spin" aria-hidden="true" /> Saving memory...</span>
<span className="inline-flex items-center gap-2"><Loader2 size={14} className="motion-safe:animate-spin" aria-hidden="true" /> {t("savingMemory")}</span>
) : feedback.status === "success" ? (
<span className="inline-flex items-center gap-2"><CheckCircle2 size={14} aria-hidden="true" /> {feedback.message}</span>
) : feedback.message ? feedback.message : "Content is validated when you leave the field or submit."
) : feedback.message ? feedback.message : t("validationHint")
}
</div>
<div className="flex items-center gap-2 pt-2">
<button type="button" onClick={closeModal} disabled={saving}
className="flex-1 py-2.5 rounded-xl text-xs font-bold cursor-pointer
bg-black/[0.04] dark:bg-white/[0.04] text-slate-500 hover:bg-black/[0.08] dark:hover:bg-white/[0.08] hover:text-slate-900 dark:hover:text-white
transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-signal-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:focus-visible:ring-offset-void-800">
Cancel
{t("cancel")}
</button>
<button type="submit"
aria-disabled={!content.trim() || saving}
Expand All @@ -196,7 +198,7 @@ export const AddMemoryModal: FunctionComponent<{
shadow-[0_2px_12px_rgba(0,224,160,0.3)]
transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-signal-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-void-800
${(!content.trim() || saving) ? "opacity-50 cursor-not-allowed" : ""}`}>
{saving ? "Saving…" : "Add Memory"}
{saving ? t("saving") : t("addMemory")}
</button>
</div>
</form>
Expand Down
Loading
Loading