324 lines
9.5 KiB
TypeScript
324 lines
9.5 KiB
TypeScript
"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>;
|
||
}
|