mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
feat(web): connect new chat UI to agent filesystem APIs
This commit is contained in:
parent
5c3a327a0c
commit
4899588cd7
5 changed files with 209 additions and 1 deletions
74
surfsense_desktop/src/modules/agent-filesystem.ts
Normal file
74
surfsense_desktop/src/modules/agent-filesystem.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { app, dialog } from "electron";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
export type AgentFilesystemMode = "cloud" | "desktop_local_folder";
|
||||
|
||||
export interface AgentFilesystemSettings {
|
||||
mode: AgentFilesystemMode;
|
||||
localRootPath: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const SETTINGS_FILENAME = "agent-filesystem-settings.json";
|
||||
|
||||
function getSettingsPath(): string {
|
||||
return join(app.getPath("userData"), SETTINGS_FILENAME);
|
||||
}
|
||||
|
||||
function getDefaultSettings(): AgentFilesystemSettings {
|
||||
return {
|
||||
mode: "cloud",
|
||||
localRootPath: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAgentFilesystemSettings(): Promise<AgentFilesystemSettings> {
|
||||
try {
|
||||
const raw = await readFile(getSettingsPath(), "utf8");
|
||||
const parsed = JSON.parse(raw) as Partial<AgentFilesystemSettings>;
|
||||
if (parsed.mode !== "cloud" && parsed.mode !== "desktop_local_folder") {
|
||||
return getDefaultSettings();
|
||||
}
|
||||
return {
|
||||
mode: parsed.mode,
|
||||
localRootPath: parsed.localRootPath ?? null,
|
||||
updatedAt: parsed.updatedAt ?? new Date().toISOString(),
|
||||
};
|
||||
} catch {
|
||||
return getDefaultSettings();
|
||||
}
|
||||
}
|
||||
|
||||
export async function setAgentFilesystemSettings(
|
||||
settings: Partial<Pick<AgentFilesystemSettings, "mode" | "localRootPath">>
|
||||
): Promise<AgentFilesystemSettings> {
|
||||
const current = await getAgentFilesystemSettings();
|
||||
const nextMode =
|
||||
settings.mode === "cloud" || settings.mode === "desktop_local_folder"
|
||||
? settings.mode
|
||||
: current.mode;
|
||||
const next: AgentFilesystemSettings = {
|
||||
mode: nextMode,
|
||||
localRootPath:
|
||||
settings.localRootPath === undefined ? current.localRootPath : settings.localRootPath,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const settingsPath = getSettingsPath();
|
||||
await mkdir(dirname(settingsPath), { recursive: true });
|
||||
await writeFile(settingsPath, JSON.stringify(next, null, 2), "utf8");
|
||||
return next;
|
||||
}
|
||||
|
||||
export async function pickAgentFilesystemRoot(): Promise<string | null> {
|
||||
const result = await dialog.showOpenDialog({
|
||||
title: "Select local folder for Agent Filesystem",
|
||||
properties: ["openDirectory"],
|
||||
});
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return result.filePaths[0] ?? null;
|
||||
}
|
||||
|
|
@ -46,6 +46,7 @@ import {
|
|||
import { useChatSessionStateSync } from "@/hooks/use-chat-session-state";
|
||||
import { useMessagesSync } from "@/hooks/use-messages-sync";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { getAgentFilesystemSelection } from "@/lib/agent-filesystem";
|
||||
import { getBearerToken } from "@/lib/auth-utils";
|
||||
import { convertToThreadMessage } from "@/lib/chat/message-utils";
|
||||
import {
|
||||
|
|
@ -656,6 +657,14 @@ export default function NewChatPage() {
|
|||
|
||||
try {
|
||||
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
|
||||
const selection = await getAgentFilesystemSelection();
|
||||
if (
|
||||
selection.filesystem_mode === "desktop_local_folder" &&
|
||||
!selection.local_filesystem_root
|
||||
) {
|
||||
toast.error("Select a local folder before using Local Folder mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build message history for context
|
||||
const messageHistory = messages
|
||||
|
|
@ -691,6 +700,9 @@ export default function NewChatPage() {
|
|||
chat_id: currentThreadId,
|
||||
user_query: userQuery.trim(),
|
||||
search_space_id: searchSpaceId,
|
||||
filesystem_mode: selection.filesystem_mode,
|
||||
client_platform: selection.client_platform,
|
||||
local_filesystem_root: selection.local_filesystem_root,
|
||||
messages: messageHistory,
|
||||
mentioned_document_ids: hasDocumentIds ? mentionedDocumentIds.document_ids : undefined,
|
||||
mentioned_surfsense_doc_ids: hasSurfsenseDocIds
|
||||
|
|
@ -1074,6 +1086,7 @@ export default function NewChatPage() {
|
|||
|
||||
try {
|
||||
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
|
||||
const selection = await getAgentFilesystemSelection();
|
||||
const response = await fetch(`${backendUrl}/api/v1/threads/${resumeThreadId}/resume`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
|
@ -1083,6 +1096,9 @@ export default function NewChatPage() {
|
|||
body: JSON.stringify({
|
||||
search_space_id: searchSpaceId,
|
||||
decisions,
|
||||
filesystem_mode: selection.filesystem_mode,
|
||||
client_platform: selection.client_platform,
|
||||
local_filesystem_root: selection.local_filesystem_root,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
|
@ -1406,6 +1422,7 @@ export default function NewChatPage() {
|
|||
]);
|
||||
|
||||
try {
|
||||
const selection = await getAgentFilesystemSelection();
|
||||
const response = await fetch(getRegenerateUrl(threadId), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
|
@ -1416,6 +1433,9 @@ export default function NewChatPage() {
|
|||
search_space_id: searchSpaceId,
|
||||
user_query: newUserQuery || null,
|
||||
disabled_tools: disabledTools.length > 0 ? disabledTools : undefined,
|
||||
filesystem_mode: selection.filesystem_mode,
|
||||
client_platform: selection.client_platform,
|
||||
local_filesystem_root: selection.local_filesystem_root,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -94,6 +94,12 @@ import { cn } from "@/lib/utils";
|
|||
|
||||
const COMPOSER_PLACEHOLDER = "Ask anything, type / for prompts, type @ to mention docs";
|
||||
|
||||
type ComposerFilesystemSettings = {
|
||||
mode: "cloud" | "desktop_local_folder";
|
||||
localRootPath: string | null;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export const Thread: FC = () => {
|
||||
return <ThreadContent />;
|
||||
};
|
||||
|
|
@ -362,6 +368,9 @@ const Composer: FC = () => {
|
|||
}, []);
|
||||
|
||||
const electronAPI = useElectronAPI();
|
||||
const [filesystemSettings, setFilesystemSettings] = useState<ComposerFilesystemSettings | null>(
|
||||
null
|
||||
);
|
||||
const [clipboardInitialText, setClipboardInitialText] = useState<string | undefined>();
|
||||
const clipboardLoadedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
|
|
@ -374,6 +383,48 @@ const Composer: FC = () => {
|
|||
});
|
||||
}, [electronAPI]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!electronAPI?.getAgentFilesystemSettings) return;
|
||||
let mounted = true;
|
||||
electronAPI
|
||||
.getAgentFilesystemSettings()
|
||||
.then((settings) => {
|
||||
if (!mounted) return;
|
||||
setFilesystemSettings(settings);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!mounted) return;
|
||||
setFilesystemSettings({
|
||||
mode: "cloud",
|
||||
localRootPath: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [electronAPI]);
|
||||
|
||||
const handleFilesystemModeChange = useCallback(
|
||||
async (mode: "cloud" | "desktop_local_folder") => {
|
||||
if (!electronAPI?.setAgentFilesystemSettings) return;
|
||||
const updated = await electronAPI.setAgentFilesystemSettings({ mode });
|
||||
setFilesystemSettings(updated);
|
||||
},
|
||||
[electronAPI]
|
||||
);
|
||||
|
||||
const handlePickFilesystemRoot = useCallback(async () => {
|
||||
if (!electronAPI?.pickAgentFilesystemRoot || !electronAPI?.setAgentFilesystemSettings) return;
|
||||
const picked = await electronAPI.pickAgentFilesystemRoot();
|
||||
if (!picked) return;
|
||||
const updated = await electronAPI.setAgentFilesystemSettings({
|
||||
mode: "desktop_local_folder",
|
||||
localRootPath: picked,
|
||||
});
|
||||
setFilesystemSettings(updated);
|
||||
}, [electronAPI]);
|
||||
|
||||
const isThreadEmpty = useAuiState(({ thread }) => thread.isEmpty);
|
||||
const isThreadRunning = useAuiState(({ thread }) => thread.isRunning);
|
||||
|
||||
|
|
@ -668,6 +719,45 @@ const Composer: FC = () => {
|
|||
currentUserId={currentUser?.id ?? null}
|
||||
members={members ?? []}
|
||||
/>
|
||||
{electronAPI && filesystemSettings ? (
|
||||
<div className="mx-3 flex items-center gap-2 rounded-xl border border-border/60 bg-background/60 p-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleFilesystemModeChange("cloud")}
|
||||
className={cn(
|
||||
"rounded-lg px-2.5 py-1 text-xs font-medium transition-colors",
|
||||
filesystemSettings.mode === "cloud"
|
||||
? "bg-accent text-foreground"
|
||||
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
Cloud
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleFilesystemModeChange("desktop_local_folder")}
|
||||
className={cn(
|
||||
"rounded-lg px-2.5 py-1 text-xs font-medium transition-colors",
|
||||
filesystemSettings.mode === "desktop_local_folder"
|
||||
? "bg-accent text-foreground"
|
||||
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
Local
|
||||
</button>
|
||||
<div className="h-4 w-px bg-border/70" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePickFilesystemRoot}
|
||||
className="min-w-0 flex-1 rounded-lg px-2.5 py-1 text-left text-xs text-muted-foreground hover:bg-accent/60 hover:text-foreground transition-colors"
|
||||
title={filesystemSettings.localRootPath ?? "Select local folder"}
|
||||
>
|
||||
{filesystemSettings.localRootPath
|
||||
? filesystemSettings.localRootPath.split("/").at(-1) || filesystemSettings.localRootPath
|
||||
: "Select folder..."}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{showDocumentPopover && (
|
||||
<div className="absolute bottom-full left-0 z-[9999] mb-2">
|
||||
<DocumentMentionPicker
|
||||
|
|
@ -1104,7 +1194,13 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
|||
group.tools.flatMap((t, i) =>
|
||||
i === 0
|
||||
? [t.description]
|
||||
: [<Dot key={i} className="inline h-4 w-4" />, t.description]
|
||||
: [
|
||||
<Dot
|
||||
key={`dot-${group.label}-${t.description}`}
|
||||
className="inline h-4 w-4"
|
||||
/>,
|
||||
t.description,
|
||||
]
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ZodType } from "zod";
|
||||
import { getClientPlatform } from "../agent-filesystem";
|
||||
import { getBearerToken, handleUnauthorized, refreshAccessToken } from "../auth-utils";
|
||||
import {
|
||||
AbortedError,
|
||||
|
|
@ -75,6 +76,8 @@ class BaseApiService {
|
|||
const defaultOptions: RequestOptions = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.bearerToken || ""}`,
|
||||
"X-SurfSense-Client-Platform":
|
||||
typeof window === "undefined" ? "web" : getClientPlatform(),
|
||||
},
|
||||
method: "GET",
|
||||
responseType: ResponseType.JSON,
|
||||
|
|
|
|||
15
surfsense_web/types/window.d.ts
vendored
15
surfsense_web/types/window.d.ts
vendored
|
|
@ -41,6 +41,14 @@ interface FolderFileEntry {
|
|||
mtimeMs: number;
|
||||
}
|
||||
|
||||
type AgentFilesystemMode = "cloud" | "desktop_local_folder";
|
||||
|
||||
interface AgentFilesystemSettings {
|
||||
mode: AgentFilesystemMode;
|
||||
localRootPath: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ElectronAPI {
|
||||
versions: {
|
||||
electron: string;
|
||||
|
|
@ -125,6 +133,13 @@ interface ElectronAPI {
|
|||
appVersion: string;
|
||||
platform: string;
|
||||
}>;
|
||||
// Agent filesystem mode
|
||||
getAgentFilesystemSettings: () => Promise<AgentFilesystemSettings>;
|
||||
setAgentFilesystemSettings: (settings: {
|
||||
mode?: AgentFilesystemMode;
|
||||
localRootPath?: string | null;
|
||||
}) => Promise<AgentFilesystemSettings>;
|
||||
pickAgentFilesystemRoot: () => Promise<string | null>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue