mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +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 { useChatSessionStateSync } from "@/hooks/use-chat-session-state";
|
||||||
import { useMessagesSync } from "@/hooks/use-messages-sync";
|
import { useMessagesSync } from "@/hooks/use-messages-sync";
|
||||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||||
|
import { getAgentFilesystemSelection } from "@/lib/agent-filesystem";
|
||||||
import { getBearerToken } from "@/lib/auth-utils";
|
import { getBearerToken } from "@/lib/auth-utils";
|
||||||
import { convertToThreadMessage } from "@/lib/chat/message-utils";
|
import { convertToThreadMessage } from "@/lib/chat/message-utils";
|
||||||
import {
|
import {
|
||||||
|
|
@ -656,6 +657,14 @@ export default function NewChatPage() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
|
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
|
// Build message history for context
|
||||||
const messageHistory = messages
|
const messageHistory = messages
|
||||||
|
|
@ -691,6 +700,9 @@ export default function NewChatPage() {
|
||||||
chat_id: currentThreadId,
|
chat_id: currentThreadId,
|
||||||
user_query: userQuery.trim(),
|
user_query: userQuery.trim(),
|
||||||
search_space_id: searchSpaceId,
|
search_space_id: searchSpaceId,
|
||||||
|
filesystem_mode: selection.filesystem_mode,
|
||||||
|
client_platform: selection.client_platform,
|
||||||
|
local_filesystem_root: selection.local_filesystem_root,
|
||||||
messages: messageHistory,
|
messages: messageHistory,
|
||||||
mentioned_document_ids: hasDocumentIds ? mentionedDocumentIds.document_ids : undefined,
|
mentioned_document_ids: hasDocumentIds ? mentionedDocumentIds.document_ids : undefined,
|
||||||
mentioned_surfsense_doc_ids: hasSurfsenseDocIds
|
mentioned_surfsense_doc_ids: hasSurfsenseDocIds
|
||||||
|
|
@ -1074,6 +1086,7 @@ export default function NewChatPage() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
|
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`, {
|
const response = await fetch(`${backendUrl}/api/v1/threads/${resumeThreadId}/resume`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -1083,6 +1096,9 @@ export default function NewChatPage() {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
search_space_id: searchSpaceId,
|
search_space_id: searchSpaceId,
|
||||||
decisions,
|
decisions,
|
||||||
|
filesystem_mode: selection.filesystem_mode,
|
||||||
|
client_platform: selection.client_platform,
|
||||||
|
local_filesystem_root: selection.local_filesystem_root,
|
||||||
}),
|
}),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
@ -1406,6 +1422,7 @@ export default function NewChatPage() {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const selection = await getAgentFilesystemSelection();
|
||||||
const response = await fetch(getRegenerateUrl(threadId), {
|
const response = await fetch(getRegenerateUrl(threadId), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -1416,6 +1433,9 @@ export default function NewChatPage() {
|
||||||
search_space_id: searchSpaceId,
|
search_space_id: searchSpaceId,
|
||||||
user_query: newUserQuery || null,
|
user_query: newUserQuery || null,
|
||||||
disabled_tools: disabledTools.length > 0 ? disabledTools : undefined,
|
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,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,12 @@ import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const COMPOSER_PLACEHOLDER = "Ask anything, type / for prompts, type @ to mention docs";
|
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 = () => {
|
export const Thread: FC = () => {
|
||||||
return <ThreadContent />;
|
return <ThreadContent />;
|
||||||
};
|
};
|
||||||
|
|
@ -362,6 +368,9 @@ const Composer: FC = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const electronAPI = useElectronAPI();
|
const electronAPI = useElectronAPI();
|
||||||
|
const [filesystemSettings, setFilesystemSettings] = useState<ComposerFilesystemSettings | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const [clipboardInitialText, setClipboardInitialText] = useState<string | undefined>();
|
const [clipboardInitialText, setClipboardInitialText] = useState<string | undefined>();
|
||||||
const clipboardLoadedRef = useRef(false);
|
const clipboardLoadedRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -374,6 +383,48 @@ const Composer: FC = () => {
|
||||||
});
|
});
|
||||||
}, [electronAPI]);
|
}, [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 isThreadEmpty = useAuiState(({ thread }) => thread.isEmpty);
|
||||||
const isThreadRunning = useAuiState(({ thread }) => thread.isRunning);
|
const isThreadRunning = useAuiState(({ thread }) => thread.isRunning);
|
||||||
|
|
||||||
|
|
@ -668,6 +719,45 @@ const Composer: FC = () => {
|
||||||
currentUserId={currentUser?.id ?? null}
|
currentUserId={currentUser?.id ?? null}
|
||||||
members={members ?? []}
|
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 && (
|
{showDocumentPopover && (
|
||||||
<div className="absolute bottom-full left-0 z-[9999] mb-2">
|
<div className="absolute bottom-full left-0 z-[9999] mb-2">
|
||||||
<DocumentMentionPicker
|
<DocumentMentionPicker
|
||||||
|
|
@ -1104,7 +1194,13 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
||||||
group.tools.flatMap((t, i) =>
|
group.tools.flatMap((t, i) =>
|
||||||
i === 0
|
i === 0
|
||||||
? [t.description]
|
? [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>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { ZodType } from "zod";
|
import type { ZodType } from "zod";
|
||||||
|
import { getClientPlatform } from "../agent-filesystem";
|
||||||
import { getBearerToken, handleUnauthorized, refreshAccessToken } from "../auth-utils";
|
import { getBearerToken, handleUnauthorized, refreshAccessToken } from "../auth-utils";
|
||||||
import {
|
import {
|
||||||
AbortedError,
|
AbortedError,
|
||||||
|
|
@ -75,6 +76,8 @@ class BaseApiService {
|
||||||
const defaultOptions: RequestOptions = {
|
const defaultOptions: RequestOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.bearerToken || ""}`,
|
Authorization: `Bearer ${this.bearerToken || ""}`,
|
||||||
|
"X-SurfSense-Client-Platform":
|
||||||
|
typeof window === "undefined" ? "web" : getClientPlatform(),
|
||||||
},
|
},
|
||||||
method: "GET",
|
method: "GET",
|
||||||
responseType: ResponseType.JSON,
|
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;
|
mtimeMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AgentFilesystemMode = "cloud" | "desktop_local_folder";
|
||||||
|
|
||||||
|
interface AgentFilesystemSettings {
|
||||||
|
mode: AgentFilesystemMode;
|
||||||
|
localRootPath: string | null;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface ElectronAPI {
|
interface ElectronAPI {
|
||||||
versions: {
|
versions: {
|
||||||
electron: string;
|
electron: string;
|
||||||
|
|
@ -125,6 +133,13 @@ interface ElectronAPI {
|
||||||
appVersion: string;
|
appVersion: string;
|
||||||
platform: string;
|
platform: string;
|
||||||
}>;
|
}>;
|
||||||
|
// Agent filesystem mode
|
||||||
|
getAgentFilesystemSettings: () => Promise<AgentFilesystemSettings>;
|
||||||
|
setAgentFilesystemSettings: (settings: {
|
||||||
|
mode?: AgentFilesystemMode;
|
||||||
|
localRootPath?: string | null;
|
||||||
|
}) => Promise<AgentFilesystemSettings>;
|
||||||
|
pickAgentFilesystemRoot: () => Promise<string | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue