Merge branch 'master' into ji

冲突解决:
- frontend-web/src/pages/AgentChatPage.tsx: 保留 HEAD 的 isLandingState +
  AgentWelcome 欢迎页扩展,丢弃 master 的旧版 InputBox 风格
- offline-backend-20260512/backend/config.yaml: 保留 HEAD 的对齐注释格式;
  master 引入的中文 bank_mission / bank_retain_mission 已自动合入

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.7 committed 2026-05-21 20:31:49 +08:00
commit a892f55cb2
18 files changed
+1043 -96

No files matched your search

+44 -7
View File
@@ -1,17 +1,43 @@
"use client";
import { useMemo, useState } from "react";
import { ArrowRight, NotebookPen, Sparkles } from "lucide-react";
import { ArrowRight, Loader2, NotebookPen, Sparkles } from "lucide-react";
import { toast } from "sonner";
import { useTheme } from "@/contexts/ThemeContext";
import { createSearchSpace } from "@/core/note/api";
import { useNoteAuth } from "@/hooks/useNoteAuth";
import { cn } from "@/lib/utils";
export default function LightAppNewNotePage() {
const { theme } = useTheme();
const isDark = theme === "dark";
const { loading: authLoading, error: authError } = useNoteAuth();
const [titleInput, setTitleInput] = useState("");
const [creating, setCreating] = useState(false);
const hasTitle = useMemo(() => titleInput.trim().length > 0, [titleInput]);
const isBusy = authLoading || creating;
const handleCreate = async () => {
const name = titleInput.trim();
if (!name || isBusy || authError) {
return;
}
setCreating(true);
try {
const searchSpace = await createSearchSpace({ name });
toast.success(`笔记「${searchSpace.name}」创建成功`);
setTitleInput("");
} catch (error) {
const message =
error instanceof Error ? error.message : "创建笔记失败,请稍后重试";
toast.error(message);
} finally {
setCreating(false);
}
};
return (
<div
@@ -21,7 +47,6 @@ export default function LightAppNewNotePage() {
)}
>
<div className="w-full max-w-[720px] -translate-y-12 space-y-8">
{/* 固定标题:新建笔记 */}
<div className="flex items-center gap-3">
<NotebookPen
className={cn("h-5 w-5 shrink-0", isDark ? "text-slate-400" : "text-black/70")}
@@ -36,12 +61,18 @@ export default function LightAppNewNotePage() {
</h1>
</div>
{/* 标题输入框 */}
<div className="flex items-center gap-3">
<input
value={titleInput}
onChange={(e) => setTitleInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
void handleCreate();
}
}}
placeholder="新建笔记"
disabled={isBusy}
className={cn(
"min-w-0 flex-1 bg-transparent text-[42px] leading-tight font-normal tracking-tight outline-none",
hasTitle
@@ -56,20 +87,25 @@ export default function LightAppNewNotePage() {
{hasTitle && (
<button
type="button"
onClick={() => void handleCreate()}
disabled={isBusy || !!authError}
className={cn(
"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition-colors",
"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-70",
isDark
? "bg-blue-500 text-white hover:bg-blue-400"
: "bg-sky-200 text-slate-700 hover:bg-sky-300",
)}
aria-label="进入笔记"
aria-label="创建笔记"
>
<ArrowRight className="h-4 w-4" />
{creating || authLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<ArrowRight className="h-4 w-4" />
)}
</button>
)}
</div>
{/* 说明文字 */}
<div
className={cn(
"flex h-11 items-center gap-2.5 rounded-xl px-4",
@@ -88,6 +124,7 @@ export default function LightAppNewNotePage() {
)}
>
新建笔记开始使用
{authError ? `(${authError})` : ""}
</p>
</div>
</div>
@@ -0,0 +1,371 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import {
BookOpen,
Grid3X3,
LayoutList,
Loader2,
MoreHorizontal,
Plus,
RefreshCw,
} from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext";
import { listSearchSpaces, type SearchSpace } from "@/core/note/api";
import { useNoteAuth } from "@/hooks/useNoteAuth";
import { cn } from "@/lib/utils";
type ViewMode = "grid" | "list";
function formatCreatedAt(value: string): string {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
return date.toLocaleDateString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
}
function getSourceCount(note: SearchSpace): number {
return note.source_count ?? note.sources_used ?? 0;
}
const cardThemes = [
"bg-[#e4f2f6]",
"bg-[#f0eaf3]",
"bg-[#edf4e6]",
"bg-[#f5eadf]",
"bg-[#e9edf8]",
];
function NoteIcon({ isDark }: { isDark: boolean }) {
return (
<div
className={cn(
"flex h-10 w-8 shrink-0 items-center justify-center rounded-sm shadow-sm",
isDark ? "bg-violet-400 text-slate-950" : "bg-[#4d3f74] text-white",
)}
>
<BookOpen className="h-5 w-5" />
</div>
);
}
export default function LightAppNoteListPage() {
const { theme } = useTheme();
const isDark = theme === "dark";
const navigate = useNavigate();
const { loading: authLoading, error: authError } = useNoteAuth();
const [notes, setNotes] = useState<SearchSpace[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<ViewMode>("grid");
const loadNotes = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await listSearchSpaces({
owned_only: true,
limit: 1000,
order_by: "created_at",
});
setNotes(data);
} catch (err) {
const message =
err instanceof Error ? err.message : "获取笔记列表失败,请稍后重试";
setError(message);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (authLoading || authError) {
return;
}
void loadNotes();
}, [authError, authLoading, loadNotes]);
const isBusy = authLoading || loading;
const createButtonClass = cn(
"group text-center transition-all hover:-translate-y-0.5",
isDark
? "border-slate-700 bg-slate-900/70 text-slate-300 hover:border-slate-500 hover:bg-slate-900"
: "border-slate-200 bg-white text-slate-600 shadow-sm hover:border-sky-200 hover:shadow-md",
);
const renderCreateButton = (layout: ViewMode) => {
if (layout === "grid") {
return (
<button
type="button"
onClick={() => navigate("/page/strategy/light-app/new-note")}
className={cn(
createButtonClass,
"flex h-[132px] flex-col items-center justify-center rounded-2xl border border-dashed",
)}
>
<span
className={cn(
"mb-3 flex h-9 w-9 items-center justify-center rounded-full transition-colors",
isDark
? "bg-slate-800 text-sky-300 group-hover:bg-slate-700"
: "bg-sky-50 text-sky-500 group-hover:bg-sky-100",
)}
>
<Plus className="h-5 w-5" />
</span>
<span className="text-base font-medium">新建笔记本</span>
</button>
);
}
return (
<button
type="button"
onClick={() => navigate("/page/strategy/light-app/new-note")}
className={cn(
createButtonClass,
"flex w-full items-center gap-4 rounded-2xl border border-dashed px-5 py-4",
)}
>
<span
className={cn(
"flex h-10 w-10 shrink-0 items-center justify-center rounded-full transition-colors",
isDark
? "bg-slate-800 text-sky-300 group-hover:bg-slate-700"
: "bg-sky-50 text-sky-500 group-hover:bg-sky-100",
)}
>
<Plus className="h-5 w-5" />
</span>
<span className="text-base font-medium">新建笔记本</span>
</button>
);
};
const renderNoteCard = (note: SearchSpace, index: number) => (
<article
key={note.id}
className={cn(
"group relative overflow-hidden rounded-2xl p-4 shadow-sm ring-1 ring-black/5 transition-all hover:-translate-y-0.5 hover:shadow-lg",
isDark
? "h-[132px] bg-slate-900 text-slate-100 ring-white/10"
: cn("h-[132px]", cardThemes[index % cardThemes.length]),
)}
>
<button
type="button"
className={cn(
"absolute right-3 top-3 rounded-full p-1.5 opacity-80 transition-colors",
isDark ? "bg-slate-800/80 text-slate-300" : "bg-white/70 text-slate-500",
)}
aria-label="更多"
>
<MoreHorizontal className="h-4 w-4" />
</button>
<NoteIcon isDark={isDark} />
<h2 className="max-w-[130px] truncate text-xl font-semibold tracking-tight">
{note.name}
</h2>
<p className={cn("mt-1 text-xs", isDark ? "text-slate-400" : "text-slate-600")}>
{formatCreatedAt(note.created_at)} · {getSourceCount(note)} 个来源
</p>
</article>
);
const renderNoteRow = (note: SearchSpace) => (
<article
key={note.id}
className={cn(
"group flex items-center gap-4 rounded-2xl px-5 py-4 shadow-sm ring-1 transition-all hover:-translate-y-0.5 hover:shadow-md",
isDark
? "bg-slate-900 text-slate-100 ring-white/10"
: "bg-white ring-black/5",
)}
>
<NoteIcon isDark={isDark} />
<div className="min-w-0 flex-1">
<h2 className="truncate text-lg font-semibold tracking-tight">{note.name}</h2>
<p className={cn("mt-1 text-sm", isDark ? "text-slate-400" : "text-slate-500")}>
{formatCreatedAt(note.created_at)} · {getSourceCount(note)} 个来源
</p>
</div>
<button
type="button"
className={cn(
"shrink-0 rounded-full p-2 opacity-80 transition-colors",
isDark ? "text-slate-400 hover:bg-slate-800" : "text-slate-500 hover:bg-slate-100",
)}
aria-label="更多"
>
<MoreHorizontal className="h-4 w-4" />
</button>
</article>
);
return (
<div
className={cn(
"h-full w-full overflow-y-auto px-10 py-10",
isDark ? "bg-slate-950" : "bg-[#fdfcfc]",
)}
>
<div className="mx-auto w-full max-w-[1120px] space-y-7">
<div className="flex items-center justify-between gap-4">
<h1
className={cn(
"text-[32px] font-semibold tracking-tight",
isDark ? "text-slate-100" : "text-slate-950",
)}
>
我的笔记本
</h1>
<div className="flex items-center gap-2">
<div
className={cn(
"flex rounded-xl p-1",
isDark ? "bg-slate-900" : "bg-white shadow-sm ring-1 ring-black/5",
)}
>
<button
type="button"
onClick={() => setViewMode("grid")}
className={cn(
"rounded-lg p-2 transition-colors",
viewMode === "grid"
? isDark
? "bg-slate-800 text-slate-100"
: "bg-slate-100 text-slate-900"
: isDark
? "text-slate-500 hover:text-slate-300"
: "text-slate-400 hover:text-slate-600",
)}
aria-label="网格视图"
aria-pressed={viewMode === "grid"}
>
<Grid3X3 className="h-4 w-4" />
</button>
<button
type="button"
onClick={() => setViewMode("list")}
className={cn(
"rounded-lg p-2 transition-colors",
viewMode === "list"
? isDark
? "bg-slate-800 text-slate-100"
: "bg-slate-100 text-slate-900"
: isDark
? "text-slate-500 hover:text-slate-300"
: "text-slate-400 hover:text-slate-600",
)}
aria-label="列表视图"
aria-pressed={viewMode === "list"}
>
<LayoutList className="h-4 w-4" />
</button>
</div>
<button
type="button"
onClick={() => void loadNotes()}
disabled={isBusy || !!authError}
className={cn(
"rounded-xl p-2 shadow-sm ring-1 ring-black/5 transition-colors disabled:cursor-not-allowed disabled:opacity-60",
isDark
? "bg-slate-900 text-slate-300 hover:bg-slate-800"
: "bg-white text-slate-600 hover:bg-slate-50",
)}
aria-label="刷新"
>
<RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} />
</button>
</div>
</div>
{authError && (
<div
className={cn(
"rounded-xl border px-4 py-3 text-sm",
isDark
? "border-red-500/30 bg-red-500/10 text-red-300"
: "border-red-200 bg-red-50 text-red-600",
)}
>
{authError}
</div>
)}
{error && !authError && (
<div
className={cn(
"rounded-xl border px-4 py-3 text-sm",
isDark
? "border-red-500/30 bg-red-500/10 text-red-300"
: "border-red-200 bg-red-50 text-red-600",
)}
>
{error}
</div>
)}
{isBusy && !authError && (
<div className="flex items-center justify-center py-24">
<Loader2
className={cn("h-6 w-6 animate-spin", isDark ? "text-slate-400" : "text-black/40")}
/>
</div>
)}
{!isBusy && !authError && !error && viewMode === "grid" && (
<div className="grid grid-cols-[repeat(auto-fill,minmax(180px,1fr))] gap-4">
{renderCreateButton("grid")}
{notes.map((note, index) => renderNoteCard(note, index))}
{notes.length === 0 && (
<div
className={cn(
"flex h-[132px] items-center justify-center rounded-2xl border text-sm",
isDark
? "border-slate-800 bg-slate-900/50 text-slate-500"
: "border-slate-100 bg-white/70 text-slate-400",
)}
>
暂无笔记
</div>
)}
</div>
)}
{!isBusy && !authError && !error && viewMode === "list" && (
<div className="space-y-3">
{renderCreateButton("list")}
{notes.map((note) => renderNoteRow(note))}
{notes.length === 0 && (
<div
className={cn(
"rounded-2xl border px-6 py-10 text-center text-sm",
isDark
? "border-slate-800 bg-slate-900/50 text-slate-500"
: "border-slate-100 bg-white/70 text-slate-400",
)}
>
暂无笔记
</div>
)}
</div>
)}
</div>
</div>
);
}
@@ -25,6 +25,7 @@ import AdminScheduledTasksPage from "./AdminScheduledTasksPage";
import AdminUserActivityPage from "./AdminUserActivityPage";
import TagsPage from "./TagsPage";
import LightAppNewNotePage from "./LightAppNewNotePage";
import LightAppNoteListPage from "./LightAppNoteListPage";
export default function StrategyRoutes() {
return (
@@ -39,6 +40,7 @@ export default function StrategyRoutes() {
{/* 轻应用 */}
<Route path="light-app/new-note" element={<LightAppNewNotePage />} />
<Route path="light-app/notes" element={<LightAppNoteListPage />} />
{/* 对话 */}
<Route path="chats" element={<ChatsPage />} />