-
+
{file.filename}
-
- {typeLabel}
+
+ {getFileTypeLabel(file.filename)}
{file.size}
@@ -404,58 +337,4 @@ function UploadedFileCard({
);
}
-/**
- * Citation link component that renders as a hover card badge
- */
-function CitationLink({
- citation,
- href,
- children,
-}: {
- citation: Citation;
- href: string;
- children: React.ReactNode;
-}) {
- const domain = extractDomainFromUrl(href);
-
- return (
-
-
- e.stopPropagation()}
- >
-
- {children ?? domain}
-
-
-
-
-
-
-
-
- );
-}
const MessageContent = memo(MessageContent_);
diff --git a/frontend/src/components/workspace/settings/memory-settings-page.tsx b/frontend/src/components/workspace/settings/memory-settings-page.tsx
index c909408..a5225e6 100644
--- a/frontend/src/components/workspace/settings/memory-settings-page.tsx
+++ b/frontend/src/components/workspace/settings/memory-settings-page.tsx
@@ -37,8 +37,6 @@ function memoryToMarkdown(
) {
const parts: string[] = [];
- console.info(memory);
-
parts.push(`## ${t.settings.memory.markdown.overview}`);
parts.push(
`- **${t.common.lastUpdated}**: \`${formatTimeAgo(memory.lastUpdated)}\``,
diff --git a/frontend/src/core/citations/index.ts b/frontend/src/core/citations/index.ts
index bf3a9eb..fd2a2aa 100644
--- a/frontend/src/core/citations/index.ts
+++ b/frontend/src/core/citations/index.ts
@@ -3,6 +3,7 @@ export {
buildCitationMap,
extractDomainFromUrl,
isCitationsBlockIncomplete,
+ removeAllCitations,
} from "./utils";
export type { Citation, ParseCitationsResult } from "./utils";
diff --git a/frontend/src/core/citations/utils.ts b/frontend/src/core/citations/utils.ts
index aadd0e1..699900b 100644
--- a/frontend/src/core/citations/utils.ts
+++ b/frontend/src/core/citations/utils.ts
@@ -76,6 +76,29 @@ export function parseCitations(content: string): ParseCitationsResult {
cleanContent = cleanContent.replace(/
[\s\S]*$/g, "").trim();
}
+ // Convert [cite-N] references to markdown links
+ // Example: [cite-1] -> [Title](url)
+ if (citations.length > 0) {
+ // Build a map from citation id to citation object
+ const idMap = new Map();
+ for (const citation of citations) {
+ idMap.set(citation.id, citation);
+ }
+
+ // Replace all [cite-N] patterns with markdown links
+ cleanContent = cleanContent.replace(/\[cite-(\d+)\]/g, (match, num) => {
+ const citeId = `cite-${num}`;
+ const citation = idMap.get(citeId);
+ if (citation) {
+ // Use title if available, otherwise use domain
+ const linkText = citation.title || extractDomainFromUrl(citation.url);
+ return `[${linkText}](${citation.url})`;
+ }
+ // If citation not found, keep the original text
+ return match;
+ });
+ }
+
return { citations, cleanContent };
}
@@ -129,3 +152,51 @@ export function isCitationsBlockIncomplete(content: string): boolean {
return hasOpenTag && !hasCloseTag;
}
+
+/**
+ * Remove ALL citations from content, including:
+ * - blocks
+ * - [cite-N] references
+ * - Citation markdown links that were converted from [cite-N]
+ *
+ * This is used for copy/download operations where we want clean content without any references.
+ *
+ * @param content - The raw content that may contain citations
+ * @returns Content with all citations completely removed
+ */
+export function removeAllCitations(content: string): string {
+ if (!content) {
+ return content;
+ }
+
+ let result = content;
+
+ // Step 1: Remove all blocks (complete and incomplete)
+ result = result.replace(/[\s\S]*?<\/citations>/g, "");
+ result = result.replace(/[\s\S]*$/g, "");
+
+ // Step 2: Remove all [cite-N] references
+ result = result.replace(/\[cite-\d+\]/g, "");
+
+ // Step 3: Parse to find citation URLs and remove those specific links
+ const parsed = parseCitations(content);
+ const citationUrls = new Set(parsed.citations.map(c => c.url));
+
+ // Remove markdown links that point to citation URLs
+ // Pattern: [text](url)
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
+ // If this URL is a citation, remove the entire link
+ if (citationUrls.has(url)) {
+ return "";
+ }
+ // Keep non-citation links
+ return match;
+ });
+
+ // Step 4: Clean up extra whitespace and newlines
+ result = result
+ .replace(/\n{3,}/g, "\n\n") // Replace 3+ newlines with 2
+ .trim();
+
+ return result;
+}
diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts
index 15475b9..c6b8687 100644
--- a/frontend/src/core/i18n/locales/en-US.ts
+++ b/frontend/src/core/i18n/locales/en-US.ts
@@ -167,6 +167,13 @@ export const enUS: Translations = {
startConversation: "Start a conversation to see messages here",
},
+ // Citations
+ citations: {
+ loadingCitations: "Organizing citations...",
+ loadingCitationsWithCount: (count: number) =>
+ `Organizing ${count} citation${count === 1 ? "" : "s"}...`,
+ },
+
// Chats
chats: {
searchChats: "Search chats",
diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts
index 58ebf09..fb69501 100644
--- a/frontend/src/core/i18n/locales/types.ts
+++ b/frontend/src/core/i18n/locales/types.ts
@@ -115,6 +115,12 @@ export interface Translations {
startConversation: string;
};
+ // Citations
+ citations: {
+ loadingCitations: string;
+ loadingCitationsWithCount: (count: number) => string;
+ };
+
// Chats
chats: {
searchChats: string;
diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts
index 3ebd23d..0242fc9 100644
--- a/frontend/src/core/i18n/locales/zh-CN.ts
+++ b/frontend/src/core/i18n/locales/zh-CN.ts
@@ -163,6 +163,12 @@ export const zhCN: Translations = {
startConversation: "开始新的对话以查看消息",
},
+ // Citations
+ citations: {
+ loadingCitations: "正在整理引用...",
+ loadingCitationsWithCount: (count: number) => `正在整理 ${count} 个引用...`,
+ },
+
// Chats
chats: {
searchChats: "搜索对话",
diff --git a/frontend/src/core/notification/hooks.ts b/frontend/src/core/notification/hooks.ts
index 102e750..e58a51d 100644
--- a/frontend/src/core/notification/hooks.ts
+++ b/frontend/src/core/notification/hooks.ts
@@ -78,7 +78,6 @@ export function useNotification(): UseNotificationReturn {
// Optional: Add event listeners
notification.onclick = () => {
- console.log("Notification clicked");
window.focus();
notification.close();
};
diff --git a/frontend/src/core/streamdown/plugins.ts b/frontend/src/core/streamdown/plugins.ts
index a3cf74f..b0d9824 100644
--- a/frontend/src/core/streamdown/plugins.ts
+++ b/frontend/src/core/streamdown/plugins.ts
@@ -5,7 +5,20 @@ import type { StreamdownProps } from "streamdown";
export const streamdownPlugins = {
remarkPlugins: [
- [remarkGfm, [remarkMath, { singleDollarTextMath: true }]],
+ remarkGfm,
+ [remarkMath, { singleDollarTextMath: true }],
+ ] as StreamdownProps["remarkPlugins"],
+ rehypePlugins: [
+ [rehypeKatex, { output: "html" }],
+ ] as StreamdownProps["rehypePlugins"],
+};
+
+// Plugins for human messages - no autolink to prevent URL bleeding into adjacent text
+export const humanMessagePlugins = {
+ remarkPlugins: [
+ // Use remark-gfm without autolink literals by not including it
+ // Only include math support for human messages
+ [remarkMath, { singleDollarTextMath: true }],
] as StreamdownProps["remarkPlugins"],
rehypePlugins: [
[rehypeKatex, { output: "html" }],
diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts
index 5d09d3c..3ecd464 100644
--- a/frontend/src/core/threads/hooks.ts
+++ b/frontend/src/core/threads/hooks.ts
@@ -207,12 +207,6 @@ export function useRenameThread() {
});
},
onSuccess(_, { threadId, title }) {
- queryClient.setQueryData(
- ["thread", "state", threadId],
- (oldData: Array) => {
- console.info("oldData", oldData);
- },
- );
queryClient.setQueriesData(
{
queryKey: ["threads", "search"],