diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8c4c599..1dc4536 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -150,3 +150,4 @@ When working on this codebase, you might need to: - `/langchain-ai/langchainjs` for LangChain - `/langchain-ai/langgraph` for LangGraph - `/quantizor/markdown-to-jsx` for Markdown to JSX conversion + - `/context7/headlessui_com` for Headless UI components diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index d65a7a7..b82f0b3 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -56,6 +56,11 @@ type Body = { type ModelStats = { modelName: string; responseTime?: number; + usage?: { + input_tokens: number; + output_tokens: number; + total_tokens: number; + }; }; const handleEmitterEvents = async ( diff --git a/src/components/ChatWindow.tsx b/src/components/ChatWindow.tsx index 6184811..f0a3273 100644 --- a/src/components/ChatWindow.tsx +++ b/src/components/ChatWindow.tsx @@ -16,6 +16,11 @@ import NextError from 'next/error'; export type ModelStats = { modelName: string; responseTime?: number; + usage?: { + input_tokens: number; + output_tokens: number; + total_tokens: number; + }; }; export type AgentActionEvent = { diff --git a/src/components/CitationLink.tsx b/src/components/CitationLink.tsx new file mode 100644 index 0000000..3428acb --- /dev/null +++ b/src/components/CitationLink.tsx @@ -0,0 +1,55 @@ +import { Document } from '@langchain/core/documents'; +import { useState } from 'react'; +import MessageSource from './MessageSource'; + +interface CitationLinkProps { + number: string; + source?: Document; + url?: string; +} + +const CitationLink = ({ number, source, url }: CitationLinkProps) => { + const [showTooltip, setShowTooltip] = useState(false); + const linkContent = ( + + {number} + + ); + + // If we have source data, wrap with tooltip + if (source) { + return ( +
+
setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)} + > + {linkContent} +
+ + {showTooltip && ( +
+
+ +
+ {/* Tooltip arrow */} +
+
+ )} +
+ ); + } + + // Otherwise, just return the plain link + return linkContent; +}; + +export default CitationLink; diff --git a/src/components/MarkdownRenderer.tsx b/src/components/MarkdownRenderer.tsx index 1d0601e..c0c9dee 100644 --- a/src/components/MarkdownRenderer.tsx +++ b/src/components/MarkdownRenderer.tsx @@ -2,7 +2,14 @@ 'use client'; import { cn } from '@/lib/utils'; -import { CheckCheck, Copy as CopyIcon, Search, FileText, Globe, Settings } from 'lucide-react'; +import { + CheckCheck, + Copy as CopyIcon, + Search, + FileText, + Globe, + Settings, +} from 'lucide-react'; import Markdown, { MarkdownToJSX } from 'markdown-to-jsx'; import { useState } from 'react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; @@ -12,6 +19,8 @@ import { } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import { useTheme } from 'next-themes'; import ThinkBox from './ThinkBox'; +import { Document } from '@langchain/core/documents'; +import CitationLink from './CitationLink'; // Helper functions for think overlay const extractThinkContent = (content: string): string | null => { @@ -51,6 +60,7 @@ interface MarkdownRendererProps { thinkBoxId: string, expanded: boolean, ) => void; + sources?: Document[]; } // Custom ToolCall component for markdown @@ -162,8 +172,8 @@ const ThinkTagProcessor = ({ onToggle?: (thinkBoxId: string, expanded: boolean) => void; }) => { return ( - { if (id && onToggle) { @@ -172,7 +182,8 @@ const ThinkTagProcessor = ({ }} /> ); -};const CodeBlock = ({ +}; +const CodeBlock = ({ className, children, }: { @@ -248,6 +259,7 @@ const MarkdownRenderer = ({ messageId, expandedThinkBoxes, onThinkBoxToggle, + sources, }: MarkdownRendererProps) => { // Preprocess content to add stable IDs to think tags const processedContent = addThinkBoxIds(content); @@ -265,7 +277,7 @@ const MarkdownRenderer = ({ }; // Determine what content to render based on showThinking parameter - const contentToRender = showThinking + const contentToRender = showThinking ? processedContent : removeThinkTags(processedContent); // Markdown formatting options @@ -317,9 +329,28 @@ const MarkdownRenderer = ({ component: ({ children }) => children, }, a: { - component: (props) => ( - - ), + component: (props) => { + // Check if this is a citation link with data-citation attribute + const citationNumber = props['data-citation']; + + if (sources && citationNumber) { + const number = parseInt(citationNumber); + const source = sources[number - 1]; + + if (source) { + return ( + + ); + } + } + + // Default link behavior + return ; + }, }, // Prevent rendering of certain HTML elements for security iframe: () => null, // Don't render iframes diff --git a/src/components/MessageActions/ModelInfo.tsx b/src/components/MessageActions/ModelInfo.tsx index 330352c..6eae9a5 100644 --- a/src/components/MessageActions/ModelInfo.tsx +++ b/src/components/MessageActions/ModelInfo.tsx @@ -48,21 +48,21 @@ const ModelInfoButton: React.FC = ({ modelStats }) => { {showPopover && (

Model Information

-
+
Model: {modelName}
{modelStats?.responseTime && ( -
+
Response time: @@ -71,6 +71,34 @@ const ModelInfoButton: React.FC = ({ modelStats }) => {
)} + {modelStats?.usage && ( + <> +
+ + Input tokens: + + + {modelStats.usage.input_tokens.toLocaleString()} + +
+
+ + Output tokens: + + + {modelStats.usage.output_tokens.toLocaleString()} + +
+
+ + Total tokens: + + + {modelStats.usage.total_tokens.toLocaleString()} + +
+ + )}
diff --git a/src/components/MessageSource.tsx b/src/components/MessageSource.tsx new file mode 100644 index 0000000..f4d197d --- /dev/null +++ b/src/components/MessageSource.tsx @@ -0,0 +1,111 @@ +/* eslint-disable @next/next/no-img-element */ +import { Document } from '@langchain/core/documents'; +import { File, Zap, Microscope, FileText, Sparkles } from 'lucide-react'; + +interface MessageSourceProps { + source: Document; + index?: number; + style?: React.CSSProperties; + className?: string; +} + +const MessageSource = ({ + source, + index, + style, + className, +}: MessageSourceProps) => { + return ( +
+ {/* Left side: Favicon/Icon and source number */} +
+ {source.metadata.url === 'File' ? ( +
+ +
+ ) : ( + favicon + )} +
+ {typeof index === 'number' && ( + {index + 1} + )} + {/* Processing type indicator */} + {source.metadata.processingType === 'preview-only' && ( + + + + )} + {source.metadata.processingType === 'full-content' && ( + + + + )} + {source.metadata.processingType === 'url-direct-content' && ( + + + + )} + {source.metadata.processingType === 'url-content-extraction' && ( + + + + )} +
+
+ + {/* Right side: Content */} +
+ {/* Title */} +

+ {source.metadata.title} +

+ + {/* URL */} +

+ {source.metadata.url.replace(/.+\/\/|www.|\..+/g, '')} +

+ + {/* Preview content */} +

+ {/* Use snippet for preview-only content, otherwise use pageContent */} + {source.metadata.processingType === 'preview-only' && + source.metadata.snippet + ? source.metadata.snippet + : source.pageContent?.length > 250 + ? source.pageContent.slice(0, 250) + '...' + : source.pageContent || 'No preview available'} +

+
+
+ ); +}; + +export default MessageSource; diff --git a/src/components/MessageSources.tsx b/src/components/MessageSources.tsx index 764cf35..a8306c7 100644 --- a/src/components/MessageSources.tsx +++ b/src/components/MessageSources.tsx @@ -1,94 +1,12 @@ /* eslint-disable @next/next/no-img-element */ import { Document } from '@langchain/core/documents'; -import { File, Zap, Microscope, FileText, Sparkles } from 'lucide-react'; +import MessageSource from './MessageSource'; const MessageSources = ({ sources }: { sources: Document[] }) => { return (
{sources.map((source, i) => ( - - {/* Left side: Favicon/Icon and source number */} -
- {source.metadata.url === 'File' ? ( -
- -
- ) : ( - favicon - )} -
- {i + 1} - {/* Processing type indicator */} - {source.metadata.processingType === 'preview-only' && ( - - - - )} - {source.metadata.processingType === 'full-content' && ( - - - - )} - {source.metadata.processingType === 'url-direct-content' && ( - - - - )} - {source.metadata.processingType === 'url-content-extraction' && ( - - - - )} -
-
- - {/* Right side: Content */} -
- {/* Title */} -

- {source.metadata.title} -

- - {/* URL */} -

- {source.metadata.url.replace(/.+\/\/|www.|\..+/g, '')} -

- - {/* Preview content */} -

- {/* Use snippet for preview-only content, otherwise use pageContent */} - {source.metadata.processingType === 'preview-only' && source.metadata.snippet - ? source.metadata.snippet - : source.pageContent?.length > 250 - ? source.pageContent.slice(0, 250) + '...' - : source.pageContent || 'No preview available' - } -

-
-
+ ))}
); diff --git a/src/components/MessageTabs.tsx b/src/components/MessageTabs.tsx index 63c5abb..71a1a5c 100644 --- a/src/components/MessageTabs.tsx +++ b/src/components/MessageTabs.tsx @@ -138,7 +138,7 @@ const MessageTabs = ({ const url = source?.metadata?.url; if (url) { - return `${numStr}`; + return `${numStr}`; } else { return `[${numStr}]`; } @@ -279,14 +279,16 @@ const MessageTabs = ({ {/* Answer Tab */} {activeTab === 'text' && (
- {loading && isLast ? null : ( + sources={message.sources} + />{' '} + {loading && isLast ? null : (
@@ -315,7 +317,6 @@ const MessageTabs = ({
)} - {isLast && message.role === 'assistant' && !loading && ( <>
diff --git a/src/components/dashboard/WidgetConfigModal.tsx b/src/components/dashboard/WidgetConfigModal.tsx index 99a6af6..5832b71 100644 --- a/src/components/dashboard/WidgetConfigModal.tsx +++ b/src/components/dashboard/WidgetConfigModal.tsx @@ -419,8 +419,13 @@ const WidgetConfigModal = ({
- - Thinking + + + Thinking +