chore: linting

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-05-28 19:21:29 -07:00
parent 4dda02c06c
commit 94e834134f
80 changed files with 443 additions and 404 deletions

View file

@ -221,10 +221,7 @@ export default async function FreeHubPage() {
<Separator className="my-12 max-w-4xl mx-auto" />
{/* In-content ad: above the model table */}
<aside
aria-label="Advertisement"
className="max-w-4xl mx-auto mb-8 min-h-[100px]"
>
<aside aria-label="Advertisement" className="max-w-4xl mx-auto mb-8 min-h-[100px]">
<AdUnit slot={ADSENSE_SLOTS.freeHubInContent} />
</aside>
@ -353,10 +350,7 @@ export default async function FreeHubPage() {
<Separator className="my-12 max-w-4xl mx-auto" />
{/* In-content ad: after CTA, before FAQ */}
<aside
aria-label="Advertisement"
className="max-w-3xl mx-auto my-8 min-h-[100px]"
>
<aside aria-label="Advertisement" className="max-w-3xl mx-auto my-8 min-h-[100px]">
<AdUnit slot={ADSENSE_SLOTS.freeHubBeforeFaq} />
</aside>

View file

@ -37,9 +37,9 @@ export default function PrivacyPolicy() {
</p>
<p className="mt-4">
By accessing or using the Service, you acknowledge that you have read and understood
this Privacy Policy. If you do not agree with our policies and practices, do not use
the Service. We may modify this policy from time to time; material changes will be
reflected by updating the "Last updated" date above.
this Privacy Policy. If you do not agree with our policies and practices, do not use the
Service. We may modify this policy from time to time; material changes will be reflected
by updating the "Last updated" date above.
</p>
</section>
@ -71,9 +71,9 @@ export default function PrivacyPolicy() {
Notion, Confluence, GitHub, and others) under the scopes you authorize.
</li>
<li>
<strong>Billing Data</strong> includes information necessary to process payments
(such as transaction identifiers and credit balances). Card details are handled by
our payment processor and are not stored on our servers.
<strong>Billing Data</strong> includes information necessary to process payments (such
as transaction identifiers and credit balances). Card details are handled by our
payment processor and are not stored on our servers.
</li>
<li>
<strong>Technical Data</strong> includes internet protocol (IP) address, browser type
@ -126,8 +126,8 @@ export default function PrivacyPolicy() {
incidents.
</li>
<li>
To communicate with you about product updates, security notices, support requests,
and (with your consent where required) marketing.
To communicate with you about product updates, security notices, support requests, and
(with your consent where required) marketing.
</li>
<li>
To serve and measure advertising on pages where ads are shown (currently, our free
@ -141,8 +141,8 @@ export default function PrivacyPolicy() {
<h2 className="text-2xl font-semibold mb-4">4. Cookies and Tracking Technologies</h2>
<p>
We and our partners use cookies, local storage, and similar technologies to operate the
Service, remember your preferences, measure usage, and serve advertising. The
categories include:
Service, remember your preferences, measure usage, and serve advertising. The categories
include:
</p>
<ul className="list-disc pl-6 my-4 space-y-2">
<li>
@ -179,9 +179,9 @@ export default function PrivacyPolicy() {
</p>
<ul className="list-disc pl-6 my-4 space-y-2">
<li>
Google, as a third-party vendor, uses cookies (including the DoubleClick DART
cookie) to serve ads to you based on your visits to our Service and other websites
on the Internet.
Google, as a third-party vendor, uses cookies (including the DoubleClick DART cookie)
to serve ads to you based on your visits to our Service and other websites on the
Internet.
</li>
<li>
Google's use of advertising cookies enables it and its partners to serve ads to you
@ -195,14 +195,12 @@ export default function PrivacyPolicy() {
<a href="https://www.youronlinechoices.com/">youronlinechoices.com</a> (EU).
</li>
<li>
For users in the European Economic Area, the United Kingdom, and Switzerland, we
use a Google-certified Consent Management Platform to obtain your consent for
personalized advertising before such cookies are set. You may change or withdraw
your consent at any time through the consent banner.
</li>
<li>
We do not knowingly serve personalized advertising to children. See Section 11.
For users in the European Economic Area, the United Kingdom, and Switzerland, we use a
Google-certified Consent Management Platform to obtain your consent for personalized
advertising before such cookies are set. You may change or withdraw your consent at
any time through the consent banner.
</li>
<li>We do not knowingly serve personalized advertising to children. See Section 11.</li>
</ul>
<p className="mt-4">
For more information about how Google uses data when you use our Service, see{" "}
@ -217,8 +215,8 @@ export default function PrivacyPolicy() {
<h2 className="text-2xl font-semibold mb-4">6. Data Security</h2>
<p>
We implement technical and organizational measures designed to protect your personal
data against accidental loss, unauthorized access, alteration, and disclosure. Access
to personal data is limited to personnel who need it to operate the Service.
data against accidental loss, unauthorized access, alteration, and disclosure. Access to
personal data is limited to personnel who need it to operate the Service.
</p>
<p className="mt-4">
No system can be guaranteed to be fully secure. We cannot guarantee that personal data
@ -232,10 +230,10 @@ export default function PrivacyPolicy() {
<p>
We retain personal data only for as long as necessary to provide the Service and to
comply with our legal, accounting, and reporting obligations. Account data is retained
for the life of your account; you can request deletion at any time. Aggregated data
that no longer identifies you may be retained indefinitely for analytics and product
improvement purposes. Anonymous chat sessions on our free pages are not retained in
any user-linked database.
for the life of your account; you can request deletion at any time. Aggregated data that
no longer identifies you may be retained indefinitely for analytics and product
improvement purposes. Anonymous chat sessions on our free pages are not retained in any
user-linked database.
</p>
</section>
@ -243,8 +241,7 @@ export default function PrivacyPolicy() {
<h2 className="text-2xl font-semibold mb-4">8. Third-Party Services</h2>
<p>
We rely on the following categories of third-party processors and providers to operate
the Service. Each is bound by its own privacy policy, which we encourage you to
review:
the Service. Each is bound by its own privacy policy, which we encourage you to review:
</p>
<ul className="list-disc pl-6 my-4 space-y-2">
<li>
@ -261,9 +258,9 @@ export default function PrivacyPolicy() {
<strong>Advertising</strong>: Google AdSense (see Section 5).
</li>
<li>
<strong>Large language model providers</strong>: OpenAI, Anthropic, Google, and
other LLM providers process the prompts and content you submit to the Service in
order to generate responses.
<strong>Large language model providers</strong>: OpenAI, Anthropic, Google, and other
LLM providers process the prompts and content you submit to the Service in order to
generate responses.
</li>
<li>
<strong>Integration providers</strong>: When you explicitly connect a third-party
@ -278,9 +275,7 @@ export default function PrivacyPolicy() {
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold mb-4">
9. Your Legal Rights (Including GDPR)
</h2>
<h2 className="text-2xl font-semibold mb-4">9. Your Legal Rights (Including GDPR)</h2>
<p>
Subject to applicable law, you have the following rights in relation to your personal
data:
@ -314,17 +309,17 @@ export default function PrivacyPolicy() {
</p>
<ul className="list-disc pl-6 my-4 space-y-2">
<li>
The right to know what categories of personal information we have collected about
you and how it is used and shared.
The right to know what categories of personal information we have collected about you
and how it is used and shared.
</li>
<li>The right to delete personal information we have collected from you.</li>
<li>The right to correct inaccurate personal information.</li>
<li>
The right to opt out of the "sale" or "sharing" of personal information for
cross-context behavioral advertising. We do not sell personal data; however,
advertising cookies set by Google AdSense may be considered "sharing" under
California law. To opt out, you can use the consent controls described in Section 5
or enable a Global Privacy Control (GPC) signal in your browser, which we honor.
advertising cookies set by Google AdSense may be considered "sharing" under California
law. To opt out, you can use the consent controls described in Section 5 or enable a
Global Privacy Control (GPC) signal in your browser, which we honor.
</li>
<li>The right not to be discriminated against for exercising your privacy rights.</li>
</ul>
@ -337,33 +332,32 @@ export default function PrivacyPolicy() {
<h2 className="text-2xl font-semibold mb-4">11. Children's Privacy</h2>
<p>
The Service is not directed to children under 13 (or under 16 in the EEA, UK, and
Switzerland). We do not knowingly collect personal data from children. If you believe
a child has provided us with personal data, please contact us and we will take steps
to delete it. We do not knowingly serve personalized advertising to children.
Switzerland). We do not knowingly collect personal data from children. If you believe a
child has provided us with personal data, please contact us and we will take steps to
delete it. We do not knowingly serve personalized advertising to children.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold mb-4">12. Changes to This Policy</h2>
<p>
We may update this Privacy Policy from time to time to reflect changes in our
practices, technology, legal requirements, or for other operational reasons. When we
make material changes, we will update the "Last updated" date at the top of this page
and, where appropriate, provide additional notice (such as an in-product notification
or email). Your continued use of the Service after the updated policy becomes
effective constitutes your acceptance of the revised policy.
We may update this Privacy Policy from time to time to reflect changes in our practices,
technology, legal requirements, or for other operational reasons. When we make material
changes, we will update the "Last updated" date at the top of this page and, where
appropriate, provide additional notice (such as an in-product notification or email).
Your continued use of the Service after the updated policy becomes effective constitutes
your acceptance of the revised policy.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold mb-4">13. Contact Us</h2>
<p>
If you have questions about this Privacy Policy or our privacy practices, or if you
want to exercise any of your rights, please contact us at:
If you have questions about this Privacy Policy or our privacy practices, or if you want
to exercise any of your rights, please contact us at:
</p>
<p className="mt-2">
<strong>Email:</strong>{" "}
<a href="mailto:rohan@surfsense.com">rohan@surfsense.com</a>
<strong>Email:</strong> <a href="mailto:rohan@surfsense.com">rohan@surfsense.com</a>
</p>
</section>
</div>

View file

@ -1,10 +1,10 @@
import { mustGetQuery } from "@rocicorp/zero";
import { handleQueryRequest } from "@rocicorp/zero/server";
import { NextResponse } from "next/server";
import { BACKEND_URL } from "@/lib/env-config";
import type { Context } from "@/types/zero";
import { queries } from "@/zero/queries";
import { schema } from "@/zero/schema";
import { BACKEND_URL } from "@/lib/env-config";
const backendURL = BACKEND_URL;

View file

@ -16,10 +16,7 @@ interface AutomationEditContentProps {
* structure but gates on ``canUpdate`` instead of ``canRead``: a user who
* can read but not update is bounced to the access-denied panel.
*/
export function AutomationEditContent({
searchSpaceId,
automationId,
}: AutomationEditContentProps) {
export function AutomationEditContent({ searchSpaceId, automationId }: AutomationEditContentProps) {
const perms = useAutomationPermissions();
const validId = Number.isInteger(automationId) && automationId > 0;
const { data: automation, isLoading, error } = useAutomation(validId ? automationId : undefined);

View file

@ -9,10 +9,7 @@ import { JsonView } from "@/components/json-view";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Spinner } from "@/components/ui/spinner";
import {
type Automation,
automationUpdateRequest,
} from "@/contracts/types/automation.types";
import { type Automation, automationUpdateRequest } from "@/contracts/types/automation.types";
interface AutomationEditFormProps {
automation: Automation;

View file

@ -69,11 +69,11 @@ import { documentsApiService } from "@/lib/apis/documents-api.service";
import { getBearerToken } from "@/lib/auth-utils";
import { type ChatFlow, classifyChatError } from "@/lib/chat/chat-error-classifier";
import { tagPreAcceptSendFailure, toHttpResponseError } from "@/lib/chat/chat-request-errors";
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
import {
convertToThreadMessage,
reconcileInterruptedAssistantMessages,
} from "@/lib/chat/message-utils";
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
import {
isPodcastGenerating,
looksLikePodcastRequest,
@ -110,6 +110,7 @@ import {
extractUserTurnForNewChatApi,
type NewChatUserImagePayload,
} from "@/lib/chat/user-turn-api-parts";
import { BACKEND_URL } from "@/lib/env-config";
import { NotFoundError } from "@/lib/error";
import {
trackChatBlocked,
@ -119,7 +120,7 @@ import {
trackChatResponseReceived,
} from "@/lib/posthog/events";
import Loading from "../loading";
import { BACKEND_URL } from "@/lib/env-config";
const MobileEditorPanel = dynamic(
() =>
import("@/components/editor-panel/editor-panel").then((m) => ({
@ -1977,14 +1978,12 @@ export default function NewChatPage() {
mentioned_folder_ids: regenerateFolderIds.length > 0 ? regenerateFolderIds : undefined,
mentioned_connector_ids:
regenerateConnectors.length > 0 ? regenerateConnectors.map((d) => d.id) : undefined,
mentioned_connectors:
regenerateConnectors.length > 0 ? regenerateConnectors : undefined,
mentioned_connectors: regenerateConnectors.length > 0 ? regenerateConnectors : undefined,
// Full mention metadata for the regenerate-specific
// source list. Only meaningful for edit (the BE only
// re-persists a user row when ``user_query`` is set);
// reload reuses the original turn's mentioned_documents.
mentioned_documents:
sourceMentionedDocs.length > 0 ? sourceMentionedDocs : undefined,
mentioned_documents: sourceMentionedDocs.length > 0 ? sourceMentionedDocs : undefined,
};
if (isEdit) {
requestBody.user_images = editExtras?.userImages ?? [];

View file

@ -31,7 +31,7 @@ import {
deleteMemberMutationAtom,
updateMemberMutationAtom,
} from "@/atoms/members/members-mutation.atoms";
import { membersAtom, myAccessAtom, canPerform } from "@/atoms/members/members-query.atoms";
import { canPerform, membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import {
AlertDialog,
AlertDialogAction,

View file

@ -42,11 +42,11 @@ export const myAccessAtom = atomWithQuery((get) => {
/**
* Helper function to check if the current user has a specific permission.
*
*
* @param access - The access object from useAtomValue(myAccessAtom)
* @param permission - The permission string to check
* @returns boolean indicating if the user has the permission
*
*
* @example
* const access = useAtomValue(myAccessAtom);
* if (canPerform(access, 'manage_members')) { ... }
@ -63,10 +63,10 @@ export function canPerform(
/**
* Hook wrapper for canPerform that reads from myAccessAtom internally.
* Use this if you want to avoid calling useAtomValue(myAccessAtom) separately.
*
*
* @param permission - The permission string to check
* @returns boolean indicating if the user has the permission
*
*
* @example
* const canManageMembers = usePermissionGate('manage_members');
*/

View file

@ -13,8 +13,8 @@ import {
CheckIcon,
ClipboardPaste,
CopyIcon,
DownloadIcon,
Dot,
DownloadIcon,
ExternalLink,
Globe,
MessageCircleReply,

View file

@ -6,14 +6,13 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { useApiKey } from "@/hooks/use-api-key";
import { BACKEND_URL } from "@/lib/env-config";
import { getConnectorBenefits } from "../connector-benefits";
import type { ConnectFormProps } from "../index";
import { BACKEND_URL } from "@/lib/env-config";
const PLUGIN_RELEASES_URL =
"https://github.com/MODSetter/SurfSense/releases?q=obsidian&expanded=true";
/**
* Obsidian connect form for the plugin-only architecture.
*

View file

@ -9,8 +9,8 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { authenticatedFetch } from "@/lib/auth-utils";
import type { ConnectorConfigProps } from "../index";
import { BACKEND_URL } from "@/lib/env-config";
import type { ConnectorConfigProps } from "../index";
export interface CirclebackConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}

View file

@ -23,6 +23,7 @@ import { LIVE_CONNECTOR_TYPES } from "../../constants/connector-constants";
import { getConnectorDisplayName } from "../../tabs/all-connectors-tab";
import { MCPServiceConfig } from "../components/mcp-service-config";
import { getConnectorConfigComponent } from "../index";
const VISION_LLM_CONNECTOR_TYPES = new Set<SearchSourceConnector["connector_type"]>([
EnumConnectorName.GOOGLE_DRIVE_CONNECTOR,
EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,

View file

@ -18,6 +18,7 @@ import { cn } from "@/lib/utils";
import { LIVE_CONNECTOR_TYPES } from "../constants/connector-constants";
import { useConnectorStatus } from "../hooks/use-connector-status";
import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
interface ConnectorAccountsListViewProps {
connectorType: string;
connectorTitle: string;

View file

@ -97,7 +97,12 @@ interface InlineMentionEditorProps {
onActionClose?: () => void;
onSubmit?: () => void;
onChange?: (text: string, docs: MentionedDocument[]) => void;
onDocumentRemove?: (docId: number, docType?: string, kind?: MentionKind, connectorType?: string) => void;
onDocumentRemove?: (
docId: number,
docType?: string,
kind?: MentionKind,
connectorType?: string
) => void;
onKeyDown?: (e: React.KeyboardEvent) => void;
disabled?: boolean;
className?: string;
@ -171,9 +176,10 @@ const MentionElement: FC<PlateElementProps<MentionElementNode>> = ({
{isFolder ? (
<FolderIcon className="h-3 w-3" />
) : isConnector ? (
getConnectorIcon(element.connector_type ?? element.document_type ?? "UNKNOWN", "h-3 w-3") ?? (
<PlugIcon className="h-3 w-3" />
)
(getConnectorIcon(
element.connector_type ?? element.document_type ?? "UNKNOWN",
"h-3 w-3"
) ?? <PlugIcon className="h-3 w-3" />)
) : (
getConnectorIcon(element.document_type ?? "UNKNOWN", "h-3 w-3")
)}
@ -357,7 +363,11 @@ function getSelectionAnchorRect(root: HTMLElement | null): SuggestionAnchorRect
const rect = range.getClientRects()[0] ?? range.getBoundingClientRect();
if (rect.width > 0 || rect.height > 0) return rectToAnchor(rect);
if (range.collapsed && range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset > 0) {
if (
range.collapsed &&
range.startContainer.nodeType === Node.TEXT_NODE &&
range.startOffset > 0
) {
const fallbackRange = range.cloneRange();
fallbackRange.setStart(range.startContainer, range.startOffset - 1);
fallbackRange.setEnd(range.startContainer, range.startOffset);

View file

@ -68,11 +68,6 @@ import {
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { UserMessage } from "@/components/assistant-ui/user-message";
import { ComposerSuggestionPopoverContent } from "@/components/new-chat/composer-suggestion-popup";
import {
DocumentMentionPicker,
promoteRecentMention,
type DocumentMentionPickerRef,
} from "../new-chat/document-mention-picker";
import { PromptPicker, type PromptPickerRef } from "@/components/new-chat/prompt-picker";
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
@ -112,6 +107,11 @@ import { captureDisplayToPngDataUrl } from "@/lib/chat/display-media-capture";
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
import { slideoutOpenedTickAtom } from "@/lib/layout-events";
import { cn } from "@/lib/utils";
import {
DocumentMentionPicker,
type DocumentMentionPickerRef,
promoteRecentMention,
} from "../new-chat/document-mention-picker";
const COMPOSER_PLACEHOLDER = "Ask anything, type / for prompts, type @ to mention docs";
@ -601,21 +601,24 @@ const Composer: FC = () => {
}
}, []);
const handleActionTrigger = useCallback((trigger: SuggestionTriggerInfo) => {
const anchorPoint = getComposerSuggestionAnchorPoint(
trigger.anchorRect,
clipboardInitialText ? "bottom" : "top"
);
if (!anchorPoint) {
setShowPromptPicker(false);
setActionQuery("");
setSuggestionAnchorPoint(null);
return;
}
setSuggestionAnchorPoint((current) => current ?? anchorPoint);
setShowPromptPicker(true);
setActionQuery(trigger.query);
}, [clipboardInitialText]);
const handleActionTrigger = useCallback(
(trigger: SuggestionTriggerInfo) => {
const anchorPoint = getComposerSuggestionAnchorPoint(
trigger.anchorRect,
clipboardInitialText ? "bottom" : "top"
);
if (!anchorPoint) {
setShowPromptPicker(false);
setActionQuery("");
setSuggestionAnchorPoint(null);
return;
}
setSuggestionAnchorPoint((current) => current ?? anchorPoint);
setShowPromptPicker(true);
setActionQuery(trigger.query);
},
[clipboardInitialText]
);
const handleActionClose = useCallback(() => {
if (showPromptPicker) {
@ -754,7 +757,12 @@ const Composer: FC = () => {
]);
const handleDocumentRemove = useCallback(
(docId: number, docType?: string, kind?: "doc" | "folder" | "connector", connectorType?: string) => {
(
docId: number,
docType?: string,
kind?: "doc" | "folder" | "connector",
connectorType?: string
) => {
setMentionedDocuments((prev) => {
const removedKey = getMentionDocKey({
id: docId,
@ -768,27 +776,30 @@ const Composer: FC = () => {
[setMentionedDocuments]
);
const handleDocumentsMention = useCallback((mentions: MentionedDocumentInfo[]) => {
const parsedSearchSpaceId = Number(search_space_id);
const editorMentionedDocs = editorRef.current?.getMentionedDocuments() ?? [];
const editorDocKeys = new Set(editorMentionedDocs.map((doc) => getMentionDocKey(doc)));
const handleDocumentsMention = useCallback(
(mentions: MentionedDocumentInfo[]) => {
const parsedSearchSpaceId = Number(search_space_id);
const editorMentionedDocs = editorRef.current?.getMentionedDocuments() ?? [];
const editorDocKeys = new Set(editorMentionedDocs.map((doc) => getMentionDocKey(doc)));
for (const mention of mentions) {
const key = getMentionDocKey(mention);
if (editorDocKeys.has(key)) continue;
editorRef.current?.insertMentionChip(mention);
if (Number.isFinite(parsedSearchSpaceId)) {
promoteRecentMention(parsedSearchSpaceId, mention);
for (const mention of mentions) {
const key = getMentionDocKey(mention);
if (editorDocKeys.has(key)) continue;
editorRef.current?.insertMentionChip(mention);
if (Number.isFinite(parsedSearchSpaceId)) {
promoteRecentMention(parsedSearchSpaceId, mention);
}
// Track within the loop so a duplicate-in-batch can't double-insert.
editorDocKeys.add(key);
}
// Track within the loop so a duplicate-in-batch can't double-insert.
editorDocKeys.add(key);
}
// Atom is reconciled by ``handleEditorChange`` via the editor's
// onChange — no second write path here.
setMentionQuery("");
setSuggestionAnchorPoint(null);
}, [search_space_id]);
// Atom is reconciled by ``handleEditorChange`` via the editor's
// onChange — no second write path here.
setMentionQuery("");
setSuggestionAnchorPoint(null);
},
[search_space_id]
);
useEffect(() => {
const editor = editorRef.current;

View file

@ -104,9 +104,9 @@ const UserTextPart: FC = () => {
const icon = isFolder ? (
<FolderIcon className="size-3.5" />
) : isConnector ? (
getConnectorIcon(segment.doc.connector_type, "size-3.5") ?? (
(getConnectorIcon(segment.doc.connector_type, "size-3.5") ?? (
<Plug className="size-3.5" />
)
))
) : (
getConnectorIcon(segment.doc.document_type ?? "UNKNOWN", "size-3.5")
);
@ -123,7 +123,9 @@ const UserTextPart: FC = () => {
: segment.doc.title
}
onClick={
isFolder || isConnector ? undefined : () => handleOpenDoc(segment.doc.id, segment.doc.title)
isFolder || isConnector
? undefined
: () => handleOpenDoc(segment.doc.id, segment.doc.title)
}
className="mx-0.5"
/>

View file

@ -34,6 +34,7 @@ import { useElectronAPI } from "@/hooks/use-platform";
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
import { inferMonacoLanguageFromPath } from "@/lib/editor-language";
import { BACKEND_URL } from "@/lib/env-config";
const PlateEditor = dynamic(
() => import("@/components/editor/plate-editor").then((m) => ({ default: m.PlateEditor })),
{ ssr: false, loading: () => <EditorPanelSkeleton /> }

View file

@ -6,11 +6,12 @@ import { Button } from "@/components/ui/button";
import type { AnonModel, AnonQuotaResponse } from "@/contracts/types/anonymous-chat.types";
import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service";
import { readSSEStream } from "@/lib/chat/streaming-state";
import { BACKEND_URL } from "@/lib/env-config";
import { trackAnonymousChatMessageSent } from "@/lib/posthog/events";
import { cn } from "@/lib/utils";
import { QuotaBar } from "./quota-bar";
import { QuotaWarningBanner } from "./quota-warning-banner";
import { BACKEND_URL } from "@/lib/env-config";
interface Message {
id: string;
role: "user" | "assistant";
@ -80,19 +81,16 @@ export function AnonymousChat({ model }: AnonymousChatProps) {
content: m.content,
}));
const response = await fetch(
`${BACKEND_URL}/api/v1/public/anon-chat/stream`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
model_slug: modelSlug,
messages: chatHistory,
}),
signal: controller.signal,
}
);
const response = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/stream`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
model_slug: modelSlug,
messages: chatHistory,
}),
signal: controller.signal,
});
if (!response.ok) {
if (response.status === 429) {

View file

@ -193,11 +193,7 @@ export function SearchSpaceAvatar({
// If delete or settings handlers are provided, expose them through a dropdown menu.
if (onDelete || onSettings) {
const trigger = (
<DropdownMenuTrigger asChild>
{avatarButton(true)}
</DropdownMenuTrigger>
);
const trigger = <DropdownMenuTrigger asChild>{avatarButton(true)}</DropdownMenuTrigger>;
return (
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>

View file

@ -78,11 +78,11 @@ import { foldersApiService } from "@/lib/apis/folders-api.service";
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
import { authenticatedFetch } from "@/lib/auth-utils";
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
import { BACKEND_URL } from "@/lib/env-config";
import { uploadFolderScan } from "@/lib/folder-sync-upload";
import { getSupportedExtensionsSet } from "@/lib/supported-extensions";
import { queries } from "@/zero/queries/index";
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
import { BACKEND_URL } from "@/lib/env-config";
const DesktopLocalTabContent = dynamic(
() => import("./DesktopLocalTabContent").then((mod) => mod.DesktopLocalTabContent),

View file

@ -11,6 +11,7 @@ import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
import { BACKEND_URL } from "@/lib/env-config";
const LARGE_DOCUMENT_THRESHOLD = 2 * 1024 * 1024; // 2MB
interface DocumentContent {

View file

@ -117,7 +117,10 @@ const ComposerSuggestionItem = React.forwardRef<
));
ComposerSuggestionItem.displayName = "ComposerSuggestionItem";
function ComposerSuggestionSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
function ComposerSuggestionSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<div className={cn("my-0.5 px-2.5", className)}>
<Separator className="bg-popover-border" {...props} />

View file

@ -2,6 +2,7 @@
import { useQuery as useZeroQuery } from "@rocicorp/zero/react";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import {
BookOpen,
ChevronLeft,
@ -22,7 +23,6 @@ import {
useState,
} from "react";
import type { MentionedDocumentInfo } from "@/atoms/chat/mentioned-documents.atom";
import { useAtomValue } from "jotai";
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
import { getConnectorTitle } from "@/components/assistant-ui/connector-popup/constants/connector-constants";
import { getConnectorDisplayName } from "@/components/assistant-ui/connector-popup/tabs/all-connectors-tab";
@ -178,7 +178,9 @@ function useDebounced<T>(value: T, delay = DEBOUNCE_MS) {
return debounced;
}
function makeDocMention(doc: Pick<Document, "id" | "title" | "document_type">): MentionedDocumentInfo {
function makeDocMention(
doc: Pick<Document, "id" | "title" | "document_type">
): MentionedDocumentInfo {
return {
id: doc.id,
title: doc.title,
@ -187,9 +189,10 @@ function makeDocMention(doc: Pick<Document, "id" | "title" | "document_type">):
};
}
function makeFolderMention(
folder: { id: number; title: string }
): Extract<MentionedDocumentInfo, { kind: "folder" }> {
function makeFolderMention(folder: {
id: number;
title: string;
}): Extract<MentionedDocumentInfo, { kind: "folder" }> {
return {
id: folder.id,
title: folder.title,
@ -319,24 +322,24 @@ export const DocumentMentionPicker = forwardRef<
useEffect(() => {
if (currentPage !== 0) return;
const combinedDocs: Pick<Document, "id" | "title" | "document_type">[] = [];
const combinedDocs: Pick<Document, "id" | "title" | "document_type">[] = [];
if (surfsenseDocs?.items) {
for (const doc of surfsenseDocs.items) {
combinedDocs.push({
id: doc.id,
title: doc.title,
document_type: "SURFSENSE_DOCS",
});
}
if (surfsenseDocs?.items) {
for (const doc of surfsenseDocs.items) {
combinedDocs.push({
id: doc.id,
title: doc.title,
document_type: "SURFSENSE_DOCS",
});
}
}
if (titleSearchResults?.items) {
combinedDocs.push(...titleSearchResults.items);
setHasMore(titleSearchResults.has_more);
}
if (titleSearchResults?.items) {
combinedDocs.push(...titleSearchResults.items);
setHasMore(titleSearchResults.has_more);
}
setAccumulatedDocuments(filterBySearchTerm(combinedDocs));
setAccumulatedDocuments(filterBySearchTerm(combinedDocs));
}, [titleSearchResults, surfsenseDocs, currentPage, filterBySearchTerm]);
const loadNextPage = useCallback(async () => {
@ -352,9 +355,11 @@ export const DocumentMentionPicker = forwardRef<
page_size: PAGE_SIZE,
...(isSearchValid ? { title: debouncedSearch.trim() } : {}),
};
const response: SearchDocumentTitlesResponse = await documentsApiService.searchDocumentTitles({
queryParams,
});
const response: SearchDocumentTitlesResponse = await documentsApiService.searchDocumentTitles(
{
queryParams,
}
);
setAccumulatedDocuments((prev) => [...prev, ...response.items]);
setHasMore(response.has_more);
@ -431,7 +436,13 @@ export const DocumentMentionPicker = forwardRef<
)
.filter((mention): mention is MentionedDocumentInfo => mention !== null)
.slice(0, RECENTS_LIMIT),
[activeConnectors, hasHydratedRecentDocs, recentMentions, recentValidationDocuments, zeroFolders]
[
activeConnectors,
hasHydratedRecentDocs,
recentMentions,
recentValidationDocuments,
zeroFolders,
]
);
const selectedKeys = useMemo(
@ -460,47 +471,46 @@ export const DocumentMentionPicker = forwardRef<
[visibleRecentMentions, selectedKeys]
);
const rootNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>(
() => {
const nodes: ComposerSuggestionNode<ResourceNodeValue>[] = [...recentRootNodes];
if (showSurfsenseDocsRoot) {
nodes.push({
id: "surfsense-docs",
label: "SurfSense Docs",
subtitle: "Browse product documentation",
icon: <BookOpen className="size-4" />,
type: "branch",
value: { kind: "view", view: { kind: "surfsense-docs" } },
});
}
nodes.push(
{
id: "files-folders",
label: "Files & Folders",
subtitle: "Browse your knowledge base",
icon: <Files className="size-4" />,
type: "branch",
value: { kind: "view", view: { kind: "files-folders" } },
},
{
id: "connectors",
label: "Connectors",
subtitle: activeConnectors.length
? "Choose the exact account for tool use"
: "No connected accounts yet",
const rootNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>(() => {
const nodes: ComposerSuggestionNode<ResourceNodeValue>[] = [...recentRootNodes];
if (showSurfsenseDocsRoot) {
nodes.push({
id: "surfsense-docs",
label: "SurfSense Docs",
subtitle: "Browse product documentation",
icon: <BookOpen className="size-4" />,
type: "branch",
value: { kind: "view", view: { kind: "surfsense-docs" } },
});
}
nodes.push(
{
id: "files-folders",
label: "Files & Folders",
subtitle: "Browse your knowledge base",
icon: <Files className="size-4" />,
type: "branch",
value: { kind: "view", view: { kind: "files-folders" } },
},
{
id: "connectors",
label: "Connectors",
subtitle: activeConnectors.length
? "Choose the exact account for tool use"
: "No connected accounts yet",
icon: <Unplug className="size-4" />,
type: "branch",
disabled: activeConnectors.length === 0,
value: { kind: "view", view: { kind: "connectors" } },
}
);
return nodes;
},
[activeConnectors.length, recentRootNodes, showSurfsenseDocsRoot]
);
type: "branch",
disabled: activeConnectors.length === 0,
value: { kind: "view", view: { kind: "connectors" } },
}
);
return nodes;
}, [activeConnectors.length, recentRootNodes, showSurfsenseDocsRoot]);
const searchNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>(() => {
const searchLower = (isSingleCharSearch ? deferredSearch : debouncedSearch).trim().toLowerCase();
const searchLower = (isSingleCharSearch ? deferredSearch : debouncedSearch)
.trim()
.toLowerCase();
const docNodes = actualDocuments.map((doc) => {
const mention = makeDocMention(doc);
return {
@ -619,7 +629,9 @@ export const DocumentMentionPicker = forwardRef<
id: getMentionDocKey(mention),
label: getConnectorDisplayName(connector.name),
subtitle: `${view.title} account`,
icon: getConnectorIcon(connector.connector_type, "size-4") ?? <Unplug className="size-4" />,
icon: getConnectorIcon(connector.connector_type, "size-4") ?? (
<Unplug className="size-4" />
),
type: "item" as const,
disabled: selectedKeys.has(getMentionDocKey(mention)),
value: { kind: "mention" as const, mention },
@ -733,7 +745,7 @@ export const DocumentMentionPicker = forwardRef<
icon={
<span className="-ml-0.5 flex size-4.5 items-center justify-center">
<ChevronLeft className="size-3.5" />
</span>
</span>
}
>
<span className="flex-1 truncate">{title}</span>
@ -759,7 +771,7 @@ export const DocumentMentionPicker = forwardRef<
return (
<Fragment key={node.id}>
{showRecentsSeparator ? <ComposerSuggestionSeparator /> : null}
<ComposerSuggestionItem
<ComposerSuggestionItem
ref={navigator.getItemRef(index)}
icon={node.icon}
selected={index === navigator.highlightedIndex}
@ -776,11 +788,11 @@ export const DocumentMentionPicker = forwardRef<
{node.subtitle}
</span>
) : null}
</span>
</span>
{node.type === "branch" ? (
<ChevronRight className="size-3.5 shrink-0 text-muted-foreground" />
) : null}
</ComposerSuggestionItem>
</ComposerSuggestionItem>
</Fragment>
);
})}

View file

@ -129,7 +129,9 @@ export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(funct
{isLoading ? (
<ComposerSuggestionSkeleton rows={8} mobileRows={8} />
) : isError ? (
<ComposerSuggestionMessage variant="destructive">Failed to load prompts</ComposerSuggestionMessage>
<ComposerSuggestionMessage variant="destructive">
Failed to load prompts
</ComposerSuggestionMessage>
) : filtered.length === 0 ? (
<ComposerSuggestionMessage>No matching prompts</ComposerSuggestionMessage>
) : (

View file

@ -12,9 +12,9 @@ import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
import { authenticatedFetch } from "@/lib/auth-utils";
import { BACKEND_URL } from "@/lib/env-config";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import { Spinner } from "../ui/spinner";
import { BACKEND_URL } from "@/lib/env-config";
interface GeneralSettingsManagerProps {
searchSpaceId: number;

View file

@ -20,10 +20,7 @@ interface PromptConfigManagerProps {
}
export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps) {
const {
data: searchSpace,
isLoading: loading,
} = useQuery({
const { data: searchSpace, isLoading: loading } = useQuery({
queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()),
queryFn: () => searchSpacesApiService.getSearchSpace({ id: searchSpaceId }),
enabled: !!searchSpaceId,
@ -56,8 +53,7 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
});
toast.success("System instructions saved successfully");
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : "Failed to save system instructions";
const message = error instanceof Error ? error.message : "Failed to save system instructions";
console.error("Error saving system instructions:", error);
toast.error(message);
}

View file

@ -16,6 +16,7 @@ import { baseApiService } from "@/lib/apis/base-api.service";
import { authenticatedFetch } from "@/lib/auth-utils";
import { clearActivePodcastTaskId, setActivePodcastTaskId } from "@/lib/chat/podcast-state";
import { BACKEND_URL } from "@/lib/env-config";
/**
* Zod schemas for runtime validation
*/
@ -193,10 +194,10 @@ function PodcastPlayer({
} else {
// Authenticated view - fetch audio and details in parallel
const [audioResponse, details] = await Promise.all([
authenticatedFetch(
`${BACKEND_URL}/api/v1/podcasts/${podcastId}/audio`,
{ method: "GET", signal: controller.signal }
),
authenticatedFetch(`${BACKEND_URL}/api/v1/podcasts/${podcastId}/audio`, {
method: "GET",
signal: controller.signal,
}),
baseApiService.get<unknown>(`/api/v1/podcasts/${podcastId}`),
]);

View file

@ -10,6 +10,7 @@ import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Button } from "@/components/ui/button";
import { baseApiService } from "@/lib/apis/base-api.service";
import { authenticatedFetch } from "@/lib/auth-utils";
import { BACKEND_URL } from "@/lib/env-config";
import { compileCheck, compileToComponent } from "@/lib/remotion/compile-check";
import { FPS } from "@/lib/remotion/constants";
import {
@ -19,7 +20,6 @@ import {
type CompiledSlide,
} from "./combined-player";
import { getPptxExportErrorToast, getVideoDownloadErrorToast } from "./errors";
import { BACKEND_URL } from "@/lib/env-config";
const GenerateVideoPresentationArgsSchema = z.object({
source_content: z.string(),

View file

@ -107,9 +107,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
setError(null);
// Build URL with optional search_space_id query parameter
const url = new URL(
`${BACKEND_URL}/api/v1/search-source-connectors`
);
const url = new URL(`${BACKEND_URL}/api/v1/search-source-connectors`);
if (spaceId !== undefined) {
url.searchParams.append("search_space_id", spaceId.toString());
}
@ -169,9 +167,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
) => {
try {
// Add search_space_id as a query parameter
const url = new URL(
`${BACKEND_URL}/api/v1/search-source-connectors`
);
const url = new URL(`${BACKEND_URL}/api/v1/search-source-connectors`);
url.searchParams.append("search_space_id", spaceId.toString());
const response = await authenticatedFetch(url.toString(), {
@ -283,9 +279,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
}
const response = await authenticatedFetch(
`${
BACKEND_URL
}/api/v1/search-source-connectors/${connectorId}/index?${params.toString()}`,
`${BACKEND_URL}/api/v1/search-source-connectors/${connectorId}/index?${params.toString()}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },

View file

@ -2,6 +2,7 @@
* Authentication utilities for handling token expiration and redirects
*/
import { BACKEND_URL } from "@/lib/env-config";
const REDIRECT_PATH_KEY = "surfsense_redirect_path";
const BEARER_TOKEN_KEY = "surfsense_bearer_token";
const REFRESH_TOKEN_KEY = "surfsense_refresh_token";

View file

@ -1,6 +1,6 @@
import type { ChatErrorKind, ChatErrorSeverity, ChatFlow } from "@/lib/chat/chat-error-classifier";
import type { ConnectorTelemetryMeta } from "@/lib/connector-telemetry";
import { getConnectorTelemetryMeta } from "@/lib/connector-telemetry";
import type { ChatErrorKind, ChatErrorSeverity, ChatFlow } from "@/lib/chat/chat-error-classifier";
/**
* PostHog Analytics Event Definitions