Files
EPEEAIKit/art-agent/frontend/src/lib/app-context.tsx

324 lines
9.5 KiB
TypeScript
Raw 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.
"use client";
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
import type { Session, Tag, ImageAsset, ChatMessage } from "./types";
import {
loadSessions,
saveSessions,
createSession as storeCreateSession,
updateSession as storeUpdateSession,
deleteSession as storeDeleteSession,
loadTags,
saveTags,
addTag as storeAddTag,
deleteTag as storeDeleteTag,
loadAssets,
addAsset as storeAddAsset,
updateAsset as storeUpdateAsset,
deleteAsset as storeDeleteAsset,
toggleFavorite as storeToggleFavorite,
generateId,
setStoreUserId,
} from "./store";
import { useAuth } from "./auth-context";
interface AppContextValue {
// 会话
sessions: Session[];
activeSessionId: string | null;
activeSession: Session | null;
createSession: () => Session;
switchSession: (id: string) => void;
deleteSession: (id: string) => void;
renameSession: (id: string, title: string) => void;
updateSessionTags: (id: string, tags: string[]) => void;
updateSessionLlmModel: (id: string, llmModel: string) => void;
appendMessage: (sessionId: string, message: ChatMessage) => void;
updateSessionThumbnail: (sessionId: string, url: string) => void;
// 标签
tags: Tag[];
addTag: (name: string, color: string) => Tag;
deleteTag: (id: string) => void;
selectedTagIds: string[];
setSelectedTagIds: (ids: string[]) => void;
// 图片资源
assets: ImageAsset[];
addAsset: (asset: ImageAsset) => void;
updateAsset: (id: string, patch: Partial<ImageAsset>) => void;
deleteAssetById: (id: string) => void;
toggleFavorite: (id: string) => boolean;
// 右侧面板
detailImage: ImageAsset | null;
setDetailImage: (asset: ImageAsset | null) => void;
// 左栏折叠
sidebarCollapsed: boolean;
setSidebarCollapsed: (v: boolean) => void;
}
const AppContext = createContext<AppContextValue | null>(null);
export function useApp(): AppContextValue {
const ctx = useContext(AppContext);
if (!ctx) throw new Error("useApp must be used within AppProvider");
return ctx;
}
export function AppProvider({ children }: { children: ReactNode }) {
const { user, isAuthenticated } = useAuth();
const [sessions, setSessions] = useState<Session[]>([]);
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
const [tags, setTags] = useState<Tag[]>([]);
const [assets, setAssets] = useState<ImageAsset[]>([]);
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
const [detailImage, setDetailImage] = useState<ImageAsset | null>(null);
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
if (typeof window !== "undefined") return window.innerWidth < 768;
return false;
});
const [initialized, setInitialized] = useState(false);
// 当用户变化时,切换 store 的 userId 并重新加载数据
useEffect(() => {
if (!isAuthenticated || !user) {
setInitialized(false);
return;
}
setStoreUserId(user.id);
setSessions(loadSessions());
setTags(loadTags());
setAssets(loadAssets());
setActiveSessionId(null);
setDetailImage(null);
setInitialized(true);
}, [user?.id, isAuthenticated]); // eslint-disable-line react-hooks/exhaustive-deps
// 初始化后,如果没有会话则自动创建一个
useEffect(() => {
if (!initialized) return;
if (sessions.length === 0) {
const s = storeCreateSession();
setSessions([s]);
setActiveSessionId(s.id);
} else if (!activeSessionId) {
setActiveSessionId(sessions[0].id);
}
}, [initialized, sessions.length, activeSessionId]);
const activeSession = useMemo(
() => sessions.find((s) => s.id === activeSessionId) ?? null,
[sessions, activeSessionId]
);
// --- 会话操作 ---
const createSession = useCallback(() => {
const s = storeCreateSession();
setSessions((prev) => [s, ...prev]);
setActiveSessionId(s.id);
setDetailImage(null);
return s;
}, []);
const switchSession = useCallback((id: string) => {
setActiveSessionId(id);
setDetailImage(null);
if (typeof window !== "undefined" && window.innerWidth < 768) {
setSidebarCollapsed(true);
}
}, []);
const deleteSessionCb = useCallback(
(id: string) => {
storeDeleteSession(id);
setSessions((prev) => {
const next = prev.filter((s) => s.id !== id);
if (activeSessionId === id) {
if (next.length > 0) {
setActiveSessionId(next[0].id);
} else {
const s = storeCreateSession();
next.push(s);
setActiveSessionId(s.id);
}
}
return next;
});
setAssets((prev) => prev.filter((a) => a.sessionId !== id));
setDetailImage(null);
},
[activeSessionId]
);
const renameSession = useCallback((id: string, title: string) => {
setSessions((prev) =>
prev.map((s) => {
if (s.id !== id) return s;
const updated = { ...s, title, updatedAt: Date.now() };
storeUpdateSession(updated);
return updated;
})
);
}, []);
const updateSessionTags = useCallback((id: string, tagIds: string[]) => {
setSessions((prev) =>
prev.map((s) => {
if (s.id !== id) return s;
const updated = { ...s, tags: tagIds, updatedAt: Date.now() };
storeUpdateSession(updated);
return updated;
})
);
}, []);
const updateSessionLlmModel = useCallback((id: string, llmModel: string) => {
setSessions((prev) =>
prev.map((s) => {
if (s.id !== id) return s;
const updated = { ...s, llmModel, updatedAt: Date.now() };
storeUpdateSession(updated);
return updated;
})
);
}, []);
const appendMessage = useCallback((sessionId: string, message: ChatMessage) => {
setSessions((prev) =>
prev.map((s) => {
if (s.id !== sessionId) return s;
const updated = {
...s,
messages: [...s.messages, message],
updatedAt: Date.now(),
};
if (message.role === "user" && s.messages.length === 0) {
updated.title = message.content.slice(0, 30) + (message.content.length > 30 ? "…" : "");
}
storeUpdateSession(updated);
return updated;
})
);
}, []);
const updateSessionThumbnail = useCallback((sessionId: string, url: string) => {
setSessions((prev) =>
prev.map((s) => {
if (s.id !== sessionId) return s;
const updated = { ...s, thumbnail: url, updatedAt: Date.now() };
storeUpdateSession(updated);
return updated;
})
);
}, []);
// --- 标签操作 ---
const addTag = useCallback((name: string, color: string) => {
const t = storeAddTag(name, color);
setTags(loadTags());
return t;
}, []);
const deleteTagCb = useCallback((id: string) => {
storeDeleteTag(id);
setTags(loadTags());
setSelectedTagIds((prev) => prev.filter((tid) => tid !== id));
}, []);
// --- 资源操作 ---
const addAssetCb = useCallback((asset: ImageAsset) => {
storeAddAsset(asset);
setAssets((prev) => [asset, ...prev]);
}, []);
const updateAssetCb = useCallback((id: string, patch: Partial<ImageAsset>) => {
storeUpdateAsset(id, patch);
setAssets((prev) => prev.map((a) => (a.id === id ? { ...a, ...patch } : a)));
setDetailImage((prev) => (prev && prev.id === id ? { ...prev, ...patch } : prev));
}, []);
const deleteAssetCb = useCallback((id: string) => {
storeDeleteAsset(id);
setAssets((prev) => prev.filter((a) => a.id !== id));
setDetailImage((prev) => (prev && prev.id === id ? null : prev));
}, []);
const toggleFavoriteCb = useCallback((id: string) => {
const result = storeToggleFavorite(id);
setAssets((prev) =>
prev.map((a) => (a.id === id ? { ...a, favorited: result } : a))
);
setDetailImage((prev) =>
prev && prev.id === id ? { ...prev, favorited: result } : prev
);
return result;
}, []);
const value = useMemo<AppContextValue>(
() => ({
sessions,
activeSessionId,
activeSession,
createSession,
switchSession,
deleteSession: deleteSessionCb,
renameSession,
updateSessionTags,
updateSessionLlmModel,
appendMessage,
updateSessionThumbnail,
tags,
addTag,
deleteTag: deleteTagCb,
selectedTagIds,
setSelectedTagIds,
assets,
addAsset: addAssetCb,
updateAsset: updateAssetCb,
deleteAssetById: deleteAssetCb,
toggleFavorite: toggleFavoriteCb,
detailImage,
setDetailImage,
sidebarCollapsed,
setSidebarCollapsed,
}),
[
sessions, activeSessionId, activeSession,
createSession, switchSession, deleteSessionCb,
renameSession, updateSessionTags, updateSessionLlmModel, appendMessage, updateSessionThumbnail,
tags, addTag, deleteTagCb, selectedTagIds,
assets, addAssetCb, updateAssetCb, deleteAssetCb, toggleFavoriteCb,
detailImage, sidebarCollapsed,
]
);
// 未登录时(如 /login 页面)直接渲染 children不注入 AppContext
// 已登录但尚未初始化完成时显示加载状态,避免子组件调用 useApp() 拿到 null
if (!initialized) {
if (isAuthenticated) {
return (
<div className="h-screen flex items-center justify-center bg-[var(--bg-primary)]">
<div className="text-[var(--text-secondary)] text-sm">...</div>
</div>
);
}
return <>{children}</>;
}
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}