mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(workspace): add daily memory file management for OpenClaw
Add browse, edit, create and delete support for daily memory files (~/.openclaw/workspace/memory/YYYY-MM-DD.md) in the Workspace panel.
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
use regex::Regex;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use crate::config::write_text_file;
|
use crate::config::write_text_file;
|
||||||
use crate::openclaw_config::get_openclaw_dir;
|
use crate::openclaw_config::get_openclaw_dir;
|
||||||
|
|
||||||
@@ -24,6 +27,146 @@ fn validate_filename(filename: &str) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Daily memory files (memory/YYYY-MM-DD.md) ---
|
||||||
|
|
||||||
|
static DAILY_MEMORY_RE: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new(r"^\d{4}-\d{2}-\d{2}\.md$").unwrap());
|
||||||
|
|
||||||
|
fn validate_daily_memory_filename(filename: &str) -> Result<(), String> {
|
||||||
|
if !DAILY_MEMORY_RE.is_match(filename) {
|
||||||
|
return Err(format!(
|
||||||
|
"Invalid daily memory filename: {filename}. Expected: YYYY-MM-DD.md"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DailyMemoryFileInfo {
|
||||||
|
pub filename: String,
|
||||||
|
pub date: String,
|
||||||
|
pub size_bytes: u64,
|
||||||
|
pub modified_at: u64,
|
||||||
|
pub preview: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Daily memory commands ---
|
||||||
|
|
||||||
|
/// List all daily memory files under `workspace/memory/`.
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn list_daily_memory_files() -> Result<Vec<DailyMemoryFileInfo>, String> {
|
||||||
|
let memory_dir = get_openclaw_dir().join("workspace").join("memory");
|
||||||
|
|
||||||
|
if !memory_dir.exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut files: Vec<DailyMemoryFileInfo> = Vec::new();
|
||||||
|
|
||||||
|
let entries = std::fs::read_dir(&memory_dir)
|
||||||
|
.map_err(|e| format!("Failed to read memory directory: {e}"))?;
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if !name.ends_with(".md") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let meta = match entry.metadata() {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
if !meta.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let date = name.trim_end_matches(".md").to_string();
|
||||||
|
|
||||||
|
let size_bytes = meta.len();
|
||||||
|
let modified_at = meta
|
||||||
|
.modified()
|
||||||
|
.ok()
|
||||||
|
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let preview = std::fs::read_to_string(entry.path())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.chars()
|
||||||
|
.take(200)
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
files.push(DailyMemoryFileInfo {
|
||||||
|
filename: name,
|
||||||
|
date,
|
||||||
|
size_bytes,
|
||||||
|
modified_at,
|
||||||
|
preview,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by modified_at descending (newest first)
|
||||||
|
files.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
|
||||||
|
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a daily memory file.
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn read_daily_memory_file(filename: String) -> Result<Option<String>, String> {
|
||||||
|
validate_daily_memory_filename(&filename)?;
|
||||||
|
|
||||||
|
let path = get_openclaw_dir()
|
||||||
|
.join("workspace")
|
||||||
|
.join("memory")
|
||||||
|
.join(&filename);
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::read_to_string(&path)
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|e| format!("Failed to read daily memory file {filename}: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a daily memory file (atomic write).
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn write_daily_memory_file(filename: String, content: String) -> Result<(), String> {
|
||||||
|
validate_daily_memory_filename(&filename)?;
|
||||||
|
|
||||||
|
let memory_dir = get_openclaw_dir().join("workspace").join("memory");
|
||||||
|
|
||||||
|
std::fs::create_dir_all(&memory_dir)
|
||||||
|
.map_err(|e| format!("Failed to create memory directory: {e}"))?;
|
||||||
|
|
||||||
|
let path = memory_dir.join(&filename);
|
||||||
|
|
||||||
|
write_text_file(&path, &content)
|
||||||
|
.map_err(|e| format!("Failed to write daily memory file {filename}: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a daily memory file (idempotent).
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_daily_memory_file(filename: String) -> Result<(), String> {
|
||||||
|
validate_daily_memory_filename(&filename)?;
|
||||||
|
|
||||||
|
let path = get_openclaw_dir()
|
||||||
|
.join("workspace")
|
||||||
|
.join("memory")
|
||||||
|
.join(&filename);
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
std::fs::remove_file(&path)
|
||||||
|
.map_err(|e| format!("Failed to delete daily memory file {filename}: {e}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Workspace file commands ---
|
||||||
|
|
||||||
/// Read an OpenClaw workspace file content.
|
/// Read an OpenClaw workspace file content.
|
||||||
/// Returns None if the file does not exist.
|
/// Returns None if the file does not exist.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -1071,6 +1071,11 @@ pub fn run() {
|
|||||||
// Workspace files (OpenClaw)
|
// Workspace files (OpenClaw)
|
||||||
commands::read_workspace_file,
|
commands::read_workspace_file,
|
||||||
commands::write_workspace_file,
|
commands::write_workspace_file,
|
||||||
|
// Daily memory files (OpenClaw workspace)
|
||||||
|
commands::list_daily_memory_files,
|
||||||
|
commands::read_daily_memory_file,
|
||||||
|
commands::write_daily_memory_file,
|
||||||
|
commands::delete_daily_memory_file,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let app = builder
|
let app = builder
|
||||||
|
|||||||
@@ -0,0 +1,304 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Calendar, Trash2, Plus } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||||
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
|
import MarkdownEditor from "@/components/MarkdownEditor";
|
||||||
|
import { workspaceApi, type DailyMemoryFileInfo } from "@/lib/api/workspace";
|
||||||
|
|
||||||
|
interface DailyMemoryPanelProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTodayFilename(): string {
|
||||||
|
const now = new Date();
|
||||||
|
const y = now.getFullYear();
|
||||||
|
const m = String(now.getMonth() + 1).padStart(2, "0");
|
||||||
|
const d = String(now.getDate()).padStart(2, "0");
|
||||||
|
return `${y}-${m}-${d}.md`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DailyMemoryPanel: React.FC<DailyMemoryPanelProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// List state
|
||||||
|
const [files, setFiles] = useState<DailyMemoryFileInfo[]>([]);
|
||||||
|
const [loadingList, setLoadingList] = useState(false);
|
||||||
|
|
||||||
|
// Edit state
|
||||||
|
const [editingFile, setEditingFile] = useState<string | null>(null);
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
const [loadingContent, setLoadingContent] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
// Delete state
|
||||||
|
const [deletingFile, setDeletingFile] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Dark mode
|
||||||
|
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||||
|
});
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class"],
|
||||||
|
});
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Load file list
|
||||||
|
const loadFiles = useCallback(async () => {
|
||||||
|
setLoadingList(true);
|
||||||
|
try {
|
||||||
|
const list = await workspaceApi.listDailyMemoryFiles();
|
||||||
|
setFiles(list);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load daily memory files:", err);
|
||||||
|
toast.error(t("workspace.dailyMemory.loadFailed"));
|
||||||
|
} finally {
|
||||||
|
setLoadingList(false);
|
||||||
|
}
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
void loadFiles();
|
||||||
|
}
|
||||||
|
}, [isOpen, loadFiles]);
|
||||||
|
|
||||||
|
// Open file for editing
|
||||||
|
const openFile = useCallback(
|
||||||
|
async (filename: string) => {
|
||||||
|
setLoadingContent(true);
|
||||||
|
setEditingFile(filename);
|
||||||
|
try {
|
||||||
|
const data = await workspaceApi.readDailyMemoryFile(filename);
|
||||||
|
setContent(data ?? "");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to read daily memory file:", err);
|
||||||
|
toast.error(t("workspace.dailyMemory.loadFailed"));
|
||||||
|
setEditingFile(null);
|
||||||
|
} finally {
|
||||||
|
setLoadingContent(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create today's note
|
||||||
|
const handleCreateToday = useCallback(async () => {
|
||||||
|
const filename = getTodayFilename();
|
||||||
|
// Check if already exists in the list
|
||||||
|
const existing = files.find((f) => f.filename === filename);
|
||||||
|
if (existing) {
|
||||||
|
// Just open it
|
||||||
|
await openFile(filename);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Create with empty content, then open
|
||||||
|
try {
|
||||||
|
await workspaceApi.writeDailyMemoryFile(filename, "");
|
||||||
|
await loadFiles();
|
||||||
|
await openFile(filename);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to create daily memory file:", err);
|
||||||
|
toast.error(t("workspace.dailyMemory.createFailed"));
|
||||||
|
}
|
||||||
|
}, [files, openFile, loadFiles, t]);
|
||||||
|
|
||||||
|
// Save current file
|
||||||
|
const handleSave = useCallback(async () => {
|
||||||
|
if (!editingFile) return;
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
await workspaceApi.writeDailyMemoryFile(editingFile, content);
|
||||||
|
toast.success(t("workspace.saveSuccess"));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save daily memory file:", err);
|
||||||
|
toast.error(t("workspace.saveFailed"));
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}, [editingFile, content, t]);
|
||||||
|
|
||||||
|
// Delete file
|
||||||
|
const handleDelete = useCallback(async () => {
|
||||||
|
if (!deletingFile) return;
|
||||||
|
try {
|
||||||
|
await workspaceApi.deleteDailyMemoryFile(deletingFile);
|
||||||
|
toast.success(t("workspace.dailyMemory.deleteSuccess"));
|
||||||
|
setDeletingFile(null);
|
||||||
|
// If we were editing this file, go back to list
|
||||||
|
if (editingFile === deletingFile) {
|
||||||
|
setEditingFile(null);
|
||||||
|
}
|
||||||
|
await loadFiles();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to delete daily memory file:", err);
|
||||||
|
toast.error(t("workspace.dailyMemory.deleteFailed"));
|
||||||
|
setDeletingFile(null);
|
||||||
|
}
|
||||||
|
}, [deletingFile, editingFile, loadFiles, t]);
|
||||||
|
|
||||||
|
// Back from edit mode to list mode
|
||||||
|
const handleBackToList = useCallback(() => {
|
||||||
|
setEditingFile(null);
|
||||||
|
setContent("");
|
||||||
|
void loadFiles();
|
||||||
|
}, [loadFiles]);
|
||||||
|
|
||||||
|
// Close panel entirely
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setEditingFile(null);
|
||||||
|
setContent("");
|
||||||
|
onClose();
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
// --- Edit mode ---
|
||||||
|
if (editingFile) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FullScreenPanel
|
||||||
|
isOpen={isOpen}
|
||||||
|
title={t("workspace.editing", { filename: editingFile })}
|
||||||
|
onClose={handleBackToList}
|
||||||
|
footer={
|
||||||
|
<Button onClick={handleSave} disabled={saving || loadingContent}>
|
||||||
|
{saving ? t("common.saving") : t("common.save")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{loadingContent ? (
|
||||||
|
<div className="flex items-center justify-center h-64 text-muted-foreground">
|
||||||
|
{t("prompts.loading")}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<MarkdownEditor
|
||||||
|
value={content}
|
||||||
|
onChange={setContent}
|
||||||
|
darkMode={isDarkMode}
|
||||||
|
placeholder={`# ${editingFile}\n\n...`}
|
||||||
|
minHeight="calc(100vh - 240px)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FullScreenPanel>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={!!deletingFile}
|
||||||
|
title={t("workspace.dailyMemory.confirmDeleteTitle")}
|
||||||
|
message={t("workspace.dailyMemory.confirmDeleteMessage", {
|
||||||
|
date: deletingFile?.replace(".md", "") ?? "",
|
||||||
|
})}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
onCancel={() => setDeletingFile(null)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- List mode ---
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FullScreenPanel
|
||||||
|
isOpen={isOpen}
|
||||||
|
title={t("workspace.dailyMemory.title")}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Header with path and create button */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
~/.openclaw/workspace/memory/
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleCreateToday}
|
||||||
|
className="gap-1.5"
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
{t("workspace.dailyMemory.createToday")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File list */}
|
||||||
|
{loadingList ? (
|
||||||
|
<div className="flex items-center justify-center h-48 text-muted-foreground">
|
||||||
|
{t("prompts.loading")}
|
||||||
|
</div>
|
||||||
|
) : files.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-48 text-muted-foreground gap-3">
|
||||||
|
<Calendar className="w-10 h-10 opacity-40" />
|
||||||
|
<p className="text-sm">{t("workspace.dailyMemory.empty")}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{files.map((file) => (
|
||||||
|
<button
|
||||||
|
key={file.filename}
|
||||||
|
onClick={() => openFile(file.filename)}
|
||||||
|
className="w-full flex items-start gap-3 p-4 rounded-xl border border-border bg-card hover:bg-accent/50 transition-colors text-left group"
|
||||||
|
>
|
||||||
|
<div className="mt-0.5 text-muted-foreground group-hover:text-foreground transition-colors">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-sm text-foreground">
|
||||||
|
{file.date}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{formatFileSize(file.sizeBytes)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{file.preview && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
||||||
|
{file.preview}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setDeletingFile(file.filename);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 text-muted-foreground hover:text-destructive transition-colors" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FullScreenPanel>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={!!deletingFile}
|
||||||
|
title={t("workspace.dailyMemory.confirmDeleteTitle")}
|
||||||
|
message={t("workspace.dailyMemory.confirmDeleteMessage", {
|
||||||
|
date: deletingFile?.replace(".md", "") ?? "",
|
||||||
|
})}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
onCancel={() => setDeletingFile(null)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DailyMemoryPanel;
|
||||||
@@ -12,10 +12,13 @@ import {
|
|||||||
Power,
|
Power,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Circle,
|
Circle,
|
||||||
|
Calendar,
|
||||||
|
ChevronRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { workspaceApi } from "@/lib/api/workspace";
|
import { workspaceApi } from "@/lib/api/workspace";
|
||||||
import WorkspaceFileEditor from "./WorkspaceFileEditor";
|
import WorkspaceFileEditor from "./WorkspaceFileEditor";
|
||||||
|
import DailyMemoryPanel from "./DailyMemoryPanel";
|
||||||
|
|
||||||
interface WorkspaceFile {
|
interface WorkspaceFile {
|
||||||
filename: string;
|
filename: string;
|
||||||
@@ -51,6 +54,7 @@ const WorkspaceFilesPanel: React.FC = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [editingFile, setEditingFile] = useState<string | null>(null);
|
const [editingFile, setEditingFile] = useState<string | null>(null);
|
||||||
const [fileExists, setFileExists] = useState<Record<string, boolean>>({});
|
const [fileExists, setFileExists] = useState<Record<string, boolean>>({});
|
||||||
|
const [showDailyMemory, setShowDailyMemory] = useState(false);
|
||||||
|
|
||||||
const checkFileExistence = async () => {
|
const checkFileExistence = async () => {
|
||||||
const results: Record<string, boolean> = {};
|
const results: Record<string, boolean> = {};
|
||||||
@@ -117,11 +121,42 @@ const WorkspaceFilesPanel: React.FC = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Daily Memory section */}
|
||||||
|
<div className="mt-8">
|
||||||
|
<h3 className="text-sm font-medium text-foreground mb-3">
|
||||||
|
{t("workspace.dailyMemory.sectionTitle")}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDailyMemory(true)}
|
||||||
|
className="w-full flex items-start gap-3 p-4 rounded-xl border border-border bg-card hover:bg-accent/50 transition-colors text-left group"
|
||||||
|
>
|
||||||
|
<div className="mt-0.5 text-muted-foreground group-hover:text-foreground transition-colors">
|
||||||
|
<Calendar className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<span className="font-medium text-sm text-foreground">
|
||||||
|
{t("workspace.dailyMemory.cardTitle")}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{t("workspace.dailyMemory.cardDescription")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-0.5 text-muted-foreground group-hover:text-foreground transition-colors">
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<WorkspaceFileEditor
|
<WorkspaceFileEditor
|
||||||
filename={editingFile ?? ""}
|
filename={editingFile ?? ""}
|
||||||
isOpen={!!editingFile}
|
isOpen={!!editingFile}
|
||||||
onClose={handleEditorClose}
|
onClose={handleEditorClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DailyMemoryPanel
|
||||||
|
isOpen={showDailyMemory}
|
||||||
|
onClose={() => setShowDailyMemory(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1166,7 +1166,21 @@
|
|||||||
"editing": "Edit {{filename}}",
|
"editing": "Edit {{filename}}",
|
||||||
"saveSuccess": "Saved successfully",
|
"saveSuccess": "Saved successfully",
|
||||||
"saveFailed": "Failed to save",
|
"saveFailed": "Failed to save",
|
||||||
"loadFailed": "Failed to load"
|
"loadFailed": "Failed to load",
|
||||||
|
"dailyMemory": {
|
||||||
|
"title": "Daily Memory",
|
||||||
|
"sectionTitle": "Daily Memory",
|
||||||
|
"cardTitle": "Daily Memory Files",
|
||||||
|
"cardDescription": "Browse and manage daily memory notes (memory/YYYY-MM-DD.md)",
|
||||||
|
"createToday": "Today's Note",
|
||||||
|
"empty": "No daily memory files yet",
|
||||||
|
"loadFailed": "Failed to load daily memory files",
|
||||||
|
"createFailed": "Failed to create daily memory file",
|
||||||
|
"deleteSuccess": "Daily memory file deleted",
|
||||||
|
"deleteFailed": "Failed to delete daily memory file",
|
||||||
|
"confirmDeleteTitle": "Delete Daily Memory",
|
||||||
|
"confirmDeleteMessage": "Delete the daily memory for {{date}}? This cannot be undone."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"openclaw": {
|
"openclaw": {
|
||||||
"env": {
|
"env": {
|
||||||
|
|||||||
@@ -1166,7 +1166,21 @@
|
|||||||
"editing": "{{filename}} を編集",
|
"editing": "{{filename}} を編集",
|
||||||
"saveSuccess": "保存しました",
|
"saveSuccess": "保存しました",
|
||||||
"saveFailed": "保存に失敗しました",
|
"saveFailed": "保存に失敗しました",
|
||||||
"loadFailed": "読み込みに失敗しました"
|
"loadFailed": "読み込みに失敗しました",
|
||||||
|
"dailyMemory": {
|
||||||
|
"title": "デイリーメモリー",
|
||||||
|
"sectionTitle": "デイリーメモリー",
|
||||||
|
"cardTitle": "デイリーメモリーファイル",
|
||||||
|
"cardDescription": "デイリーメモリーノートの閲覧と管理(memory/YYYY-MM-DD.md)",
|
||||||
|
"createToday": "今日のノート",
|
||||||
|
"empty": "デイリーメモリーファイルはまだありません",
|
||||||
|
"loadFailed": "デイリーメモリーファイルの読み込みに失敗しました",
|
||||||
|
"createFailed": "デイリーメモリーファイルの作成に失敗しました",
|
||||||
|
"deleteSuccess": "デイリーメモリーファイルを削除しました",
|
||||||
|
"deleteFailed": "デイリーメモリーファイルの削除に失敗しました",
|
||||||
|
"confirmDeleteTitle": "デイリーメモリーを削除",
|
||||||
|
"confirmDeleteMessage": "{{date}} のデイリーメモリーを削除しますか?この操作は取り消せません。"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"openclaw": {
|
"openclaw": {
|
||||||
"env": {
|
"env": {
|
||||||
|
|||||||
@@ -1166,7 +1166,21 @@
|
|||||||
"editing": "编辑 {{filename}}",
|
"editing": "编辑 {{filename}}",
|
||||||
"saveSuccess": "保存成功",
|
"saveSuccess": "保存成功",
|
||||||
"saveFailed": "保存失败",
|
"saveFailed": "保存失败",
|
||||||
"loadFailed": "读取失败"
|
"loadFailed": "读取失败",
|
||||||
|
"dailyMemory": {
|
||||||
|
"title": "每日记忆",
|
||||||
|
"sectionTitle": "每日记忆",
|
||||||
|
"cardTitle": "每日记忆文件",
|
||||||
|
"cardDescription": "浏览和管理每日记忆笔记(memory/YYYY-MM-DD.md)",
|
||||||
|
"createToday": "今日笔记",
|
||||||
|
"empty": "暂无每日记忆文件",
|
||||||
|
"loadFailed": "加载每日记忆文件失败",
|
||||||
|
"createFailed": "创建每日记忆文件失败",
|
||||||
|
"deleteSuccess": "每日记忆文件已删除",
|
||||||
|
"deleteFailed": "删除每日记忆文件失败",
|
||||||
|
"confirmDeleteTitle": "删除每日记忆",
|
||||||
|
"confirmDeleteMessage": "确定删除 {{date}} 的每日记忆吗?此操作不可撤销。"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"openclaw": {
|
"openclaw": {
|
||||||
"env": {
|
"env": {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
export interface DailyMemoryFileInfo {
|
||||||
|
filename: string;
|
||||||
|
date: string;
|
||||||
|
sizeBytes: number;
|
||||||
|
modifiedAt: number;
|
||||||
|
preview: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const workspaceApi = {
|
export const workspaceApi = {
|
||||||
async readFile(filename: string): Promise<string | null> {
|
async readFile(filename: string): Promise<string | null> {
|
||||||
return invoke<string | null>("read_workspace_file", { filename });
|
return invoke<string | null>("read_workspace_file", { filename });
|
||||||
@@ -8,4 +16,20 @@ export const workspaceApi = {
|
|||||||
async writeFile(filename: string, content: string): Promise<void> {
|
async writeFile(filename: string, content: string): Promise<void> {
|
||||||
return invoke<void>("write_workspace_file", { filename, content });
|
return invoke<void>("write_workspace_file", { filename, content });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async listDailyMemoryFiles(): Promise<DailyMemoryFileInfo[]> {
|
||||||
|
return invoke<DailyMemoryFileInfo[]>("list_daily_memory_files");
|
||||||
|
},
|
||||||
|
|
||||||
|
async readDailyMemoryFile(filename: string): Promise<string | null> {
|
||||||
|
return invoke<string | null>("read_daily_memory_file", { filename });
|
||||||
|
},
|
||||||
|
|
||||||
|
async writeDailyMemoryFile(filename: string, content: string): Promise<void> {
|
||||||
|
return invoke<void>("write_daily_memory_file", { filename, content });
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteDailyMemoryFile(filename: string): Promise<void> {
|
||||||
|
return invoke<void>("delete_daily_memory_file", { filename });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user