mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-20 23:21:06 +02:00
Merge remote-tracking branch 'upstream/dev' into fix/onboarding
This commit is contained in:
commit
2d837ea18c
185 changed files with 4729 additions and 3630 deletions
|
|
@ -35,6 +35,7 @@ import {
|
|||
useAllCitationMetadata,
|
||||
} from "@/components/assistant-ui/citation-metadata-context";
|
||||
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
||||
import { MessageTimestamp } from "@/components/assistant-ui/message-timestamp";
|
||||
import { ReasoningMessagePart } from "@/components/assistant-ui/reasoning-message-part";
|
||||
import { RevertTurnButton } from "@/components/assistant-ui/revert-turn-button";
|
||||
import {
|
||||
|
|
@ -62,6 +63,7 @@ import { withArtifactAnchor } from "@/features/chat-artifacts";
|
|||
import { useComments } from "@/hooks/use-comments";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
import { formatMessageTimestamp } from "@/lib/format-date";
|
||||
import { getProviderIcon } from "@/lib/provider-icons";
|
||||
import { tryGetHostname } from "@/lib/url";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -249,16 +251,6 @@ export const MessageError: FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
function formatMessageDate(date: Date): string {
|
||||
return date.toLocaleDateString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format provider USD cost (in micro-USD) for inline display next to a
|
||||
* token count. Falls back to ``"<$0.001"`` for sub-tenth-of-a-cent
|
||||
|
|
@ -367,7 +359,7 @@ const MessageInfoDropdown: FC<{ chatTurnId: string | null | undefined }> = ({ ch
|
|||
>
|
||||
{createdAt && (
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground font-normal select-none">
|
||||
{formatMessageDate(createdAt)}
|
||||
{formatMessageTimestamp(createdAt)}
|
||||
</DropdownMenuLabel>
|
||||
)}
|
||||
{hasUsage && (
|
||||
|
|
@ -463,6 +455,8 @@ const AssistantMessageInner: FC = () => {
|
|||
<MessageError />
|
||||
</div>
|
||||
|
||||
<MessageTimestamp className="ml-2 mt-2" />
|
||||
|
||||
{isMobile && (
|
||||
<div className="ml-2 mt-2">
|
||||
<MobileCitationDrawer />
|
||||
|
|
|
|||
24
surfsense_web/components/assistant-ui/message-timestamp.tsx
Normal file
24
surfsense_web/components/assistant-ui/message-timestamp.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { useAuiState } from "@assistant-ui/react";
|
||||
import { useAtomValue } from "jotai";
|
||||
import type { FC } from "react";
|
||||
import { showMessageTimestampsAtom } from "@/atoms/chat/show-timestamps.atom";
|
||||
import { formatMessageTimestamp } from "@/lib/format-date";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Muted, always-visible timestamp under a chat message. Renders only when the
|
||||
* user has opted in via {@link showMessageTimestampsAtom} and the message
|
||||
* carries a ``createdAt`` (absent on optimistic pre-persist messages).
|
||||
*/
|
||||
export const MessageTimestamp: FC<{ className?: string }> = ({ className }) => {
|
||||
const show = useAtomValue(showMessageTimestampsAtom);
|
||||
const createdAt = useAuiState(({ message }) => message?.createdAt);
|
||||
|
||||
if (!show || !createdAt) return null;
|
||||
|
||||
return (
|
||||
<div className={cn("select-none text-[11px] text-muted-foreground", className)}>
|
||||
{formatMessageTimestamp(createdAt)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1070,10 +1070,7 @@ const ConnectedScraperIcons: FC<{ workspaceId: number }> = ({ workspaceId }) =>
|
|||
return (
|
||||
<Tooltip key={platform.id}>
|
||||
<TooltipTrigger asChild>
|
||||
<Avatar
|
||||
className="size-5"
|
||||
style={{ zIndex: platforms.length - i }}
|
||||
>
|
||||
<Avatar className="size-5" style={{ zIndex: platforms.length - i }}>
|
||||
<AvatarFallback className="bg-popover text-[10px]">
|
||||
<Icon className="size-3" />
|
||||
</AvatarFallback>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { currentThreadAtom } from "@/atoms/chat/current-thread.atom";
|
|||
import { messageDocumentsMapAtom } from "@/atoms/chat/mentioned-documents.atom";
|
||||
import { openEditorPanelAtom } from "@/atoms/editor/editor-panel.atom";
|
||||
import { MentionChip } from "@/components/assistant-ui/mention-chip";
|
||||
import { MessageTimestamp } from "@/components/assistant-ui/message-timestamp";
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
|
||||
|
|
@ -182,6 +183,7 @@ export const UserMessage: FC = () => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<MessageTimestamp className="mt-1 pl-1" />
|
||||
</div>
|
||||
</MessagePrimitive.Root>
|
||||
);
|
||||
|
|
|
|||
23
surfsense_web/components/chat/active-chat-stream-runner.tsx
Normal file
23
surfsense_web/components/chat/active-chat-stream-runner.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { chatStreamStore } from "@/lib/chat/stream-engine/store";
|
||||
|
||||
/**
|
||||
* Persistent, render-null host that scopes the in-flight chat turn's lifetime
|
||||
* to the workspace shell, not the chat page.
|
||||
*
|
||||
* Mounted in ``LayoutDataProvider``, it survives in-app navigation between
|
||||
* workspace routes and aborts the single active turn only on workspace/app
|
||||
* teardown, so ordinary navigation disconnects the view without stopping the
|
||||
* stream.
|
||||
*/
|
||||
export function ActiveChatStreamRunner() {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
chatStreamStore.abortActive();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -27,7 +27,8 @@ export type HeroChatDemoScript = {
|
|||
|
||||
type Stage = "typing" | "steps" | "answer" | "done";
|
||||
|
||||
const PLACEHOLDER = "Track competitors, scrape platforms, automate briefs. Use / for prompts, @ for docs";
|
||||
const PLACEHOLDER =
|
||||
"Track competitors, scrape platforms, automate briefs. Use / for prompts, @ for docs";
|
||||
|
||||
/** Blinking caret for the typewriter (overlay only, never inside the real input). */
|
||||
function Caret() {
|
||||
|
|
|
|||
|
|
@ -708,7 +708,7 @@ export function HeroSection() {
|
|||
"relative mt-4 max-w-4xl text-left text-4xl font-bold tracking-tight text-balance text-neutral-900 sm:text-5xl md:text-6xl dark:text-neutral-50"
|
||||
)}
|
||||
>
|
||||
<Balancer>Give your AI agents competitive intelligence.</Balancer>
|
||||
<Balancer>NotebookLM for competitive intelligence research.</Balancer>
|
||||
</h1>
|
||||
<div className="mt-4 flex w-full flex-col items-start justify-between gap-4 md:mt-8 md:flex-row md:items-end md:gap-10">
|
||||
<div>
|
||||
|
|
@ -717,9 +717,10 @@ export function HeroSection() {
|
|||
"relative mb-8 max-w-2xl text-left text-sm text-neutral-600 antialiased sm:text-base md:text-lg dark:text-neutral-400"
|
||||
)}
|
||||
>
|
||||
SurfSense is an open-source competitive intelligence platform. Your AI agents monitor
|
||||
competitors, track rankings, and listen to your market with live data from platforms
|
||||
like Reddit, YouTube, Instagram, TikTok, Google Maps, Google Search, and the open web.
|
||||
SurfSense is an open-source competitive intelligence platform, like NotebookLM but
|
||||
with live scraping connectors. Your AI agents monitor competitors, track rankings, and
|
||||
listen to your market with live data from platforms like Reddit, YouTube, Instagram,
|
||||
TikTok, Google Maps, Google Search, and the open web.
|
||||
</p>
|
||||
|
||||
<div className="relative mb-4 flex w-full flex-col justify-center gap-y-2 sm:flex-row sm:justify-start sm:space-y-0 sm:space-x-4">
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export { default as PerplexityIcon } from "./perplexity.svg";
|
|||
export { default as QwenIcon } from "./qwen.svg";
|
||||
export { default as RecraftIcon } from "./recraft.svg";
|
||||
export { default as ReplicateIcon } from "./replicate.svg";
|
||||
export { default as RequestyIcon } from "./requesty.svg";
|
||||
export { default as SambaNovaIcon } from "./sambanova.svg";
|
||||
export { default as TogetherAiIcon } from "./togetherai.svg";
|
||||
export { default as VertexAiIcon } from "./vertexai.svg";
|
||||
|
|
|
|||
1
surfsense_web/components/icons/providers/requesty.svg
Normal file
1
surfsense_web/components/icons/providers/requesty.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 2a1 1 0 000 2h2.086l-5.293 5.293a1 1 0 001.414 1.414L18 5.414V7.5a1 1 0 102 0v-4.5a1 1 0 00-1-1h-4.5zM4 6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4a1 1 0 10-2 0v4H4V8h4a1 1 0 000-2H4z"></path></svg>
|
||||
|
After Width: | Height: | Size: 318 B |
|
|
@ -18,6 +18,7 @@ import { workspacesAtom } from "@/atoms/workspaces/workspace-query.atoms";
|
|||
import { ActionLogDialog } from "@/components/agent-action-log/action-log-dialog";
|
||||
import { AnnouncementSpotlight } from "@/components/announcements/AnnouncementSpotlight";
|
||||
import { AnnouncementsDialog } from "@/components/announcements/AnnouncementsDialog";
|
||||
import { ActiveChatStreamRunner } from "@/components/chat/active-chat-stream-runner";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
|
@ -649,6 +650,9 @@ export function LayoutDataProvider({
|
|||
|
||||
return (
|
||||
<>
|
||||
{/* Persistent host: keeps an in-flight chat turn streaming across
|
||||
in-app navigation and aborts it only on workspace teardown. */}
|
||||
<ActiveChatStreamRunner />
|
||||
<LayoutShell
|
||||
workspaces={workspaces}
|
||||
activeWorkspaceId={Number(workspaceId)}
|
||||
|
|
|
|||
|
|
@ -324,9 +324,7 @@ export function NotificationsDropdown({
|
|||
)}
|
||||
>
|
||||
<span>{tab.label}</span>
|
||||
<span
|
||||
className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-muted px-1.5 text-[11px] font-semibold text-muted-foreground"
|
||||
>
|
||||
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-muted px-1.5 text-[11px] font-semibold text-muted-foreground">
|
||||
{formatNotificationCount(tab.count)}
|
||||
</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -198,8 +198,8 @@ export function AutoReloadSettings() {
|
|||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Last top-up failed</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your saved card was declined and top-ups were turned off. Update your card and
|
||||
re-enable top-ups below.
|
||||
Your saved card was declined and top-ups were turned off. Update your card and re-enable
|
||||
top-ups below.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
|
@ -290,7 +290,11 @@ export function AutoReloadSettings() {
|
|||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button className="w-full sm:w-auto" onClick={handleSave} disabled={saveMutation.isPending}>
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
onClick={handleSave}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
{saveMutation.isPending ? (
|
||||
<>
|
||||
<Spinner size="xs" />
|
||||
|
|
|
|||
|
|
@ -9,9 +9,17 @@ function baseUrlHint(provider: string) {
|
|||
return "For local servers, use host.docker.internal instead of localhost.";
|
||||
}
|
||||
if (provider === "openai_compatible") {
|
||||
return "Enter the full endpoint URL.";
|
||||
return "Enter the full endpoint URL. This provider expects a /v1-compatible endpoint.";
|
||||
}
|
||||
if (provider === "openai" || provider === "anthropic" || provider === "openrouter") {
|
||||
if (provider === "openai_compatible_raw") {
|
||||
return "Enter the exact chat-completions API base URL. SurfSense will not append /v1.";
|
||||
}
|
||||
if (
|
||||
provider === "openai" ||
|
||||
provider === "anthropic" ||
|
||||
provider === "openrouter" ||
|
||||
provider === "requesty"
|
||||
) {
|
||||
return "Override only if you route through a proxy or gateway.";
|
||||
}
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ export const PROVIDER_ORDER = [
|
|||
"bedrock",
|
||||
"azure",
|
||||
"openrouter",
|
||||
"requesty",
|
||||
"ollama_chat",
|
||||
"lm_studio",
|
||||
"openai_compatible",
|
||||
"openai_compatible_raw",
|
||||
];
|
||||
|
||||
export const PROVIDER_DISPLAY: Record<
|
||||
|
|
@ -37,12 +39,23 @@ export const PROVIDER_DISPLAY: Record<
|
|||
subtitle: "OpenAI-compatible endpoint",
|
||||
iconKey: "custom",
|
||||
},
|
||||
openai_compatible_raw: {
|
||||
name: "OpenAI-Compatible Raw",
|
||||
subtitle: "Use the exact base URL, no /v1 is appended",
|
||||
iconKey: "custom",
|
||||
},
|
||||
openrouter: {
|
||||
name: "OpenRouter",
|
||||
subtitle: "OpenRouter",
|
||||
iconKey: "openrouter",
|
||||
defaultBaseUrl: "https://openrouter.ai/api/v1",
|
||||
},
|
||||
requesty: {
|
||||
name: "Requesty",
|
||||
subtitle: "Requesty",
|
||||
iconKey: "requesty",
|
||||
defaultBaseUrl: "https://router.requesty.ai/v1",
|
||||
},
|
||||
vertex_ai: { name: "Gemini", subtitle: "Google Cloud Vertex AI", iconKey: "vertex_ai" },
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,12 @@ export function WorkspaceApiAccessControl({
|
|||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-3 md:flex-row md:items-center md:justify-between", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-3 md:flex-row md:items-center md:justify-between",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-3 w-56" />
|
||||
|
|
@ -71,7 +76,12 @@ export function WorkspaceApiAccessControl({
|
|||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-3 md:flex-row md:items-center md:justify-between", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-3 md:flex-row md:items-center md:justify-between",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Label>API key access</Label>
|
||||
<p className="text-xs text-destructive">Failed to load workspace API access.</p>
|
||||
|
|
@ -84,7 +94,12 @@ export function WorkspaceApiAccessControl({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-3 md:flex-row md:items-center md:justify-between", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-3 md:flex-row md:items-center md:justify-between",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="api-access-enabled">API key access</Label>
|
||||
<p className="text-xs text-muted-foreground">Allow API keys to access this workspace.</p>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue