mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-15 18:25:18 +02:00
feat: enhance web search tool integration with citation management and UI enhancements
This commit is contained in:
parent
9eab427b56
commit
74826b3714
13 changed files with 133 additions and 209 deletions
|
|
@ -12,6 +12,7 @@ import type { FC } from "react";
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { commentsEnabledAtom, targetCommentIdAtom } from "@/atoms/chat/current-thread.atom";
|
||||
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||
import { CitationMetadataProvider } from "@/components/assistant-ui/citation-metadata-context";
|
||||
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
||||
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
|
|
@ -78,7 +79,7 @@ export const MessageError: FC = () => {
|
|||
|
||||
const AssistantMessageInner: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<CitationMetadataProvider>
|
||||
<div className="aui-assistant-message-content wrap-break-word px-2 text-foreground leading-relaxed">
|
||||
<MessagePrimitive.Parts
|
||||
components={{
|
||||
|
|
@ -116,9 +117,10 @@ const AssistantMessageInner: FC = () => {
|
|||
create_confluence_page: CreateConfluencePageToolUI,
|
||||
update_confluence_page: UpdateConfluencePageToolUI,
|
||||
delete_confluence_page: DeleteConfluencePageToolUI,
|
||||
link_preview: () => null,
|
||||
multi_link_preview: () => null,
|
||||
scrape_webpage: () => null,
|
||||
web_search: () => null,
|
||||
link_preview: () => null,
|
||||
multi_link_preview: () => null,
|
||||
scrape_webpage: () => null,
|
||||
},
|
||||
Fallback: ToolFallback,
|
||||
},
|
||||
|
|
@ -130,7 +132,7 @@ const AssistantMessageInner: FC = () => {
|
|||
<div className="aui-assistant-message-footer mt-1 mb-5 ml-2 flex">
|
||||
<AssistantActionBar />
|
||||
</div>
|
||||
</>
|
||||
</CitationMetadataProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
"use client";
|
||||
|
||||
import { useAuiState } from "@assistant-ui/react";
|
||||
import { createContext, type FC, type ReactNode, useContext, useMemo } from "react";
|
||||
|
||||
export interface CitationMeta {
|
||||
title: string;
|
||||
snippet?: string;
|
||||
}
|
||||
|
||||
type CitationMetadataMap = ReadonlyMap<string, CitationMeta>;
|
||||
|
||||
const CitationMetadataContext = createContext<CitationMetadataMap>(new Map());
|
||||
|
||||
interface ToolCallResult {
|
||||
status?: string;
|
||||
citations?: Record<string, { title: string; snippet?: string }>;
|
||||
}
|
||||
|
||||
interface MessageContent {
|
||||
type: string;
|
||||
toolName?: string;
|
||||
result?: unknown;
|
||||
}
|
||||
|
||||
export const CitationMetadataProvider: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const content = useAuiState(({ message }) => (message as { content?: MessageContent[] })?.content);
|
||||
|
||||
const metadataMap = useMemo<CitationMetadataMap>(() => {
|
||||
if (!content || !Array.isArray(content)) return new Map();
|
||||
|
||||
const merged = new Map<string, CitationMeta>();
|
||||
|
||||
for (const part of content) {
|
||||
if (part.type !== "tool-call" || part.toolName !== "web_search" || !part.result) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = part.result as ToolCallResult;
|
||||
const citations = result.citations;
|
||||
if (!citations || typeof citations !== "object") continue;
|
||||
|
||||
for (const [url, meta] of Object.entries(citations)) {
|
||||
if (url.startsWith("http") && meta.title && !merged.has(url)) {
|
||||
merged.set(url, { title: meta.title, snippet: meta.snippet });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}, [content]);
|
||||
|
||||
return (
|
||||
<CitationMetadataContext.Provider value={metadataMap}>{children}</CitationMetadataContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useCitationMetadata(url: string): CitationMeta | undefined {
|
||||
const map = useContext(CitationMetadataContext);
|
||||
return map.get(url);
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { useCitationMetadata } from "@/components/assistant-ui/citation-metadata-context";
|
||||
import { SourceDetailPanel } from "@/components/new-chat/source-detail-panel";
|
||||
import { Citation } from "@/components/tool-ui/citation";
|
||||
|
||||
interface InlineCitationProps {
|
||||
chunkId: number;
|
||||
|
|
@ -55,21 +56,23 @@ interface UrlCitationProps {
|
|||
|
||||
/**
|
||||
* Inline citation for live web search results (URL-based chunk IDs).
|
||||
* Renders a clickable badge showing the source domain that opens the URL in a new tab.
|
||||
* Renders a compact chip with favicon + domain and a hover popover showing the
|
||||
* page title and snippet (extracted deterministically from web_search tool results).
|
||||
*/
|
||||
export const UrlCitation: FC<UrlCitationProps> = ({ url }) => {
|
||||
const domain = extractDomain(url);
|
||||
const meta = useCitationMetadata(url);
|
||||
|
||||
return (
|
||||
<a
|
||||
<Citation
|
||||
id={`url-cite-${url}`}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[10px] font-bold bg-primary/80 hover:bg-primary text-primary-foreground rounded-full h-4 px-1.5 inline-flex items-center gap-0.5 align-super cursor-pointer transition-colors ml-0.5 no-underline"
|
||||
title={url}
|
||||
>
|
||||
<ExternalLink className="size-2.5 shrink-0" />
|
||||
{domain}
|
||||
</a>
|
||||
title={meta?.title || domain}
|
||||
snippet={meta?.snippet}
|
||||
domain={domain}
|
||||
favicon={`https://www.google.com/s2/favicons?domain=${domain}&sz=32`}
|
||||
variant="inline"
|
||||
type="webpage"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue