mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-23 19:05:16 +02:00
Merge remote-tracking branch 'upstream/dev' into feat/ui-revamp
This commit is contained in:
commit
f65bc81509
603 changed files with 45035 additions and 4652 deletions
|
|
@ -405,35 +405,19 @@ class ConnectorsApiService {
|
|||
);
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// MCP Tool Trust (Allow-List) Methods
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Add a tool to the MCP connector's "Always Allow" list.
|
||||
* Subsequent calls to this tool will skip HITL approval.
|
||||
*/
|
||||
trustMCPTool = async (connectorId: number, toolName: string): Promise<void> => {
|
||||
await baseApiService.post(`/api/v1/connectors/mcp/${connectorId}/trust-tool`, undefined, {
|
||||
body: { tool_name: toolName },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a tool from the MCP connector's "Always Allow" list.
|
||||
*/
|
||||
untrustMCPTool = async (connectorId: number, toolName: string): Promise<void> => {
|
||||
await baseApiService.post(`/api/v1/connectors/mcp/${connectorId}/untrust-tool`, undefined, {
|
||||
body: { tool_name: toolName },
|
||||
});
|
||||
};
|
||||
|
||||
/** Live stats for the Obsidian connector tile. */
|
||||
getObsidianStats = async (vaultId: string): Promise<ObsidianStats> => {
|
||||
return baseApiService.get<ObsidianStats>(
|
||||
`/api/v1/obsidian/stats?vault_id=${encodeURIComponent(vaultId)}`
|
||||
);
|
||||
};
|
||||
|
||||
/** Revoke a previously-trusted MCP tool so the next call asks again. */
|
||||
untrustMCPTool = async (connectorId: number, toolName: string): Promise<void> => {
|
||||
await baseApiService.post(`/api/v1/connectors/mcp/${connectorId}/untrust-tool`, undefined, {
|
||||
body: { tool_name: toolName },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export interface ObsidianStats {
|
||||
|
|
|
|||
54
surfsense_web/lib/blog-faq.ts
Normal file
54
surfsense_web/lib/blog-faq.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export interface FaqEntry {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts FAQ items from a blog post MDX file by parsing the `## FAQ` section.
|
||||
*
|
||||
* The FAQ section is bounded by `## FAQ` and the next H2 heading. Each H3 inside
|
||||
* is treated as a question, with the body until the next H3 (or the end of the
|
||||
* FAQ section) as its answer. Common Markdown decorations (links, bold, inline
|
||||
* code) are stripped so the JSON-LD output contains plain text suitable for
|
||||
* Google's FAQ rich-result eligibility checks.
|
||||
*
|
||||
* Returns an empty array when the post has no FAQ section, or when the file
|
||||
* cannot be read (e.g. for posts that do not yet exist on disk).
|
||||
*/
|
||||
export async function extractFaqFromBlogPost(slug: string): Promise<FaqEntry[]> {
|
||||
try {
|
||||
const filepath = path.join(process.cwd(), "blog", "content", `${slug}.mdx`);
|
||||
const content = await fs.readFile(filepath, "utf-8");
|
||||
return extractFaqFromContent(content);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function extractFaqFromContent(content: string): FaqEntry[] {
|
||||
const faqHeading = content.match(/^##\s+FAQ\s*$/m);
|
||||
if (!faqHeading || faqHeading.index === undefined) return [];
|
||||
|
||||
const afterFaq = content.slice(faqHeading.index + faqHeading[0].length);
|
||||
const nextH2 = afterFaq.match(/^##\s+/m);
|
||||
const faqBody = nextH2 ? afterFaq.slice(0, nextH2.index) : afterFaq;
|
||||
|
||||
const blocks = faqBody.split(/^###\s+/m).slice(1);
|
||||
|
||||
return blocks
|
||||
.map((block) => {
|
||||
const newlineIdx = block.indexOf("\n");
|
||||
const question = (newlineIdx === -1 ? block : block.slice(0, newlineIdx)).trim();
|
||||
const answer = (newlineIdx === -1 ? "" : block.slice(newlineIdx + 1))
|
||||
.trim()
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
||||
.replace(/`([^`]+)`/g, "$1")
|
||||
.replace(/\s+/g, " ");
|
||||
return { question, answer };
|
||||
})
|
||||
.filter((item) => item.question.length > 0 && item.answer.length > 0);
|
||||
}
|
||||
|
|
@ -565,7 +565,7 @@ export type SSEEvent =
|
|||
* the assistant-side row of the current turn. The frontend
|
||||
* renames its optimistic ``msg-assistant-XXX`` placeholder
|
||||
* id, migrates the local ``tokenUsageStore`` and
|
||||
* ``pendingInterrupt`` references, and binds the running
|
||||
* ``pendingInterrupts`` entries, and binds the running
|
||||
* mutable ``assistantMsgId`` closure variable to the
|
||||
* canonical id for the rest of the stream.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -22,3 +22,11 @@ export function formatRelativeDate(dateString: string): string {
|
|||
if (daysAgo < 7) return `${daysAgo}d ago`;
|
||||
return format(date, "MMM d, yyyy");
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a thread's last-updated timestamp for the chats sidebars.
|
||||
* Example: "Mar 23, 2026 at 4:30 PM"
|
||||
*/
|
||||
export function formatThreadTimestamp(dateString: string): string {
|
||||
return format(new Date(dateString), "MMM d, yyyy 'at' h:mm a");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue