mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-27 19:25:15 +02:00
feat: add folder management features including creation, deletion, and organization of documents within folders
This commit is contained in:
parent
95bb522220
commit
685ad0c02d
41 changed files with 7475 additions and 4330 deletions
217
surfsense_web/atoms/tabs/tabs.atom.ts
Normal file
217
surfsense_web/atoms/tabs/tabs.atom.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import { atom } from "jotai";
|
||||
|
||||
export type TabType = "chat" | "document";
|
||||
|
||||
export interface Tab {
|
||||
id: string;
|
||||
type: TabType;
|
||||
title: string;
|
||||
/** For chat tabs */
|
||||
chatId?: number | null;
|
||||
chatUrl?: string;
|
||||
/** For document tabs */
|
||||
documentId?: number;
|
||||
searchSpaceId?: number;
|
||||
}
|
||||
|
||||
interface TabsState {
|
||||
tabs: Tab[];
|
||||
activeTabId: string | null;
|
||||
}
|
||||
|
||||
const INITIAL_CHAT_TAB: Tab = {
|
||||
id: "chat-new",
|
||||
type: "chat",
|
||||
title: "New Chat",
|
||||
chatId: null,
|
||||
chatUrl: undefined,
|
||||
};
|
||||
|
||||
const initialState: TabsState = {
|
||||
tabs: [INITIAL_CHAT_TAB],
|
||||
activeTabId: "chat-new",
|
||||
};
|
||||
|
||||
export const tabsStateAtom = atom<TabsState>(initialState);
|
||||
|
||||
export const tabsAtom = atom((get) => get(tabsStateAtom).tabs);
|
||||
export const activeTabIdAtom = atom((get) => get(tabsStateAtom).activeTabId);
|
||||
export const activeTabAtom = atom((get) => {
|
||||
const state = get(tabsStateAtom);
|
||||
return state.tabs.find((t) => t.id === state.activeTabId) ?? null;
|
||||
});
|
||||
|
||||
function makeChatTabId(chatId: number | null): string {
|
||||
return chatId ? `chat-${chatId}` : "chat-new";
|
||||
}
|
||||
|
||||
function makeDocumentTabId(documentId: number): string {
|
||||
return `doc-${documentId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the current chat from Next.js routing into the tab bar.
|
||||
* If a tab for this chat already exists, activate it.
|
||||
* Otherwise, replace the "new chat" tab or create one.
|
||||
*/
|
||||
export const syncChatTabAtom = atom(
|
||||
null,
|
||||
(
|
||||
get,
|
||||
set,
|
||||
{
|
||||
chatId,
|
||||
title,
|
||||
chatUrl,
|
||||
}: { chatId: number | null; title?: string; chatUrl?: string }
|
||||
) => {
|
||||
const state = get(tabsStateAtom);
|
||||
const tabId = makeChatTabId(chatId);
|
||||
const existing = state.tabs.find((t) => t.id === tabId);
|
||||
|
||||
if (existing) {
|
||||
set(tabsStateAtom, {
|
||||
...state,
|
||||
activeTabId: tabId,
|
||||
tabs: state.tabs.map((t) =>
|
||||
t.id === tabId
|
||||
? { ...t, title: title || t.title, chatUrl: chatUrl || t.chatUrl }
|
||||
: t
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If navigating to a new chat (no chatId), ensure there's a "new chat" tab
|
||||
if (!chatId) {
|
||||
const hasNewChatTab = state.tabs.some((t) => t.id === "chat-new");
|
||||
if (hasNewChatTab) {
|
||||
set(tabsStateAtom, { ...state, activeTabId: "chat-new" });
|
||||
} else {
|
||||
set(tabsStateAtom, {
|
||||
tabs: [...state.tabs, INITIAL_CHAT_TAB],
|
||||
activeTabId: "chat-new",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace the "new chat" tab if it exists and is empty, otherwise add new tab
|
||||
const newChatTabIdx = state.tabs.findIndex((t) => t.id === "chat-new");
|
||||
const newTab: Tab = {
|
||||
id: tabId,
|
||||
type: "chat",
|
||||
title: title || `Chat ${chatId}`,
|
||||
chatId,
|
||||
chatUrl,
|
||||
};
|
||||
|
||||
let updatedTabs: Tab[];
|
||||
if (newChatTabIdx !== -1) {
|
||||
updatedTabs = [...state.tabs];
|
||||
updatedTabs[newChatTabIdx] = newTab;
|
||||
} else {
|
||||
updatedTabs = [...state.tabs, newTab];
|
||||
}
|
||||
|
||||
set(tabsStateAtom, { tabs: updatedTabs, activeTabId: tabId });
|
||||
}
|
||||
);
|
||||
|
||||
/** Update the title of the current chat tab (e.g., when a chat gets its first response). */
|
||||
export const updateChatTabTitleAtom = atom(
|
||||
null,
|
||||
(get, set, { chatId, title }: { chatId: number; title: string }) => {
|
||||
const state = get(tabsStateAtom);
|
||||
const tabId = makeChatTabId(chatId);
|
||||
set(tabsStateAtom, {
|
||||
...state,
|
||||
tabs: state.tabs.map((t) => (t.id === tabId ? { ...t, title } : t)),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
/** Open a document tab. If already open, just switch to it. */
|
||||
export const openDocumentTabAtom = atom(
|
||||
null,
|
||||
(
|
||||
get,
|
||||
set,
|
||||
{
|
||||
documentId,
|
||||
searchSpaceId,
|
||||
title,
|
||||
}: { documentId: number; searchSpaceId: number; title?: string }
|
||||
) => {
|
||||
const state = get(tabsStateAtom);
|
||||
const tabId = makeDocumentTabId(documentId);
|
||||
const existing = state.tabs.find((t) => t.id === tabId);
|
||||
|
||||
if (existing) {
|
||||
set(tabsStateAtom, {
|
||||
...state,
|
||||
activeTabId: tabId,
|
||||
tabs: state.tabs.map((t) =>
|
||||
t.id === tabId ? { ...t, title: title || t.title } : t
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newTab: Tab = {
|
||||
id: tabId,
|
||||
type: "document",
|
||||
title: title || `Document ${documentId}`,
|
||||
documentId,
|
||||
searchSpaceId,
|
||||
};
|
||||
|
||||
set(tabsStateAtom, {
|
||||
tabs: [...state.tabs, newTab],
|
||||
activeTabId: tabId,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
/** Switch to a tab by ID. Returns the tab so the caller can navigate if needed. */
|
||||
export const switchTabAtom = atom(null, (get, set, tabId: string) => {
|
||||
const state = get(tabsStateAtom);
|
||||
const tab = state.tabs.find((t) => t.id === tabId);
|
||||
if (tab) {
|
||||
set(tabsStateAtom, { ...state, activeTabId: tabId });
|
||||
}
|
||||
return tab ?? null;
|
||||
});
|
||||
|
||||
/** Close a tab. If it was active, activate the nearest sibling. */
|
||||
export const closeTabAtom = atom(null, (get, set, tabId: string) => {
|
||||
const state = get(tabsStateAtom);
|
||||
const idx = state.tabs.findIndex((t) => t.id === tabId);
|
||||
if (idx === -1) return null;
|
||||
|
||||
const remaining = state.tabs.filter((t) => t.id !== tabId);
|
||||
|
||||
// Don't close the last tab — always keep at least one
|
||||
if (remaining.length === 0) {
|
||||
set(tabsStateAtom, {
|
||||
tabs: [INITIAL_CHAT_TAB],
|
||||
activeTabId: "chat-new",
|
||||
});
|
||||
return INITIAL_CHAT_TAB;
|
||||
}
|
||||
|
||||
let newActiveId = state.activeTabId;
|
||||
if (state.activeTabId === tabId) {
|
||||
// Activate the tab to the left (or right if first)
|
||||
const newIdx = Math.min(idx, remaining.length - 1);
|
||||
newActiveId = remaining[newIdx].id;
|
||||
}
|
||||
|
||||
set(tabsStateAtom, { tabs: remaining, activeTabId: newActiveId });
|
||||
return remaining.find((t) => t.id === newActiveId) ?? null;
|
||||
});
|
||||
|
||||
/** Reset tabs when switching search spaces. */
|
||||
export const resetTabsAtom = atom(null, (_get, set) => {
|
||||
set(tabsStateAtom, { ...initialState });
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue