11import React from 'react' ;
22import type { Source } from '../types' ;
33import { LoadingSpinner } from './LoadingSpinner' ;
4- import { LinkIcon , ResearchDeeperIcon } from './icons' ;
4+ import { LinkIcon , ResearchDeeperIcon , DownloadIcon } from './icons' ;
55
66interface ResultsDisplayProps {
77 text : string ;
@@ -16,10 +16,10 @@ const SourceLink: React.FC<{ source: Source }> = ({ source }) => (
1616 href = { source . web . uri }
1717 target = "_blank"
1818 rel = "noopener noreferrer"
19- className = "flex items-center gap-2 text-sm bg-slate-700/50 p-2 rounded-md hover:bg-slate-600/50 transition duration-200 "
19+ className = "flex items-center gap-2 p-2 text-sm transition duration-200 rounded-md bg-slate-700/50 hover:bg-slate-600/50"
2020 >
2121 < LinkIcon />
22- < span className = "truncate text-teal-300 hover:text-teal-200" > { source . web . title || new URL ( source . web . uri ) . hostname } </ span >
22+ < span className = "text-teal-300 truncate hover:text-teal-200" > { source . web . title || new URL ( source . web . uri ) . hostname } </ span >
2323 </ a >
2424) ;
2525
@@ -49,6 +49,32 @@ const ResultItem: React.FC<{ line: string; onResearchDeeper: (query: string) =>
4949
5050
5151export const ResultsDisplay : React . FC < ResultsDisplayProps > = ( { text, sources, isLoading, error, onResearchDeeper } ) => {
52+ const handleDownload = ( ) => {
53+ const timestamp = new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] ;
54+ const filename = `aeternum-research-${ timestamp } .md` ;
55+
56+ let content = `# Aeternum Research Findings\n\n` ;
57+ content += `Generated on: ${ new Date ( ) . toLocaleString ( ) } \n\n` ;
58+ content += `## Research Results\n\n${ text } \n\n` ;
59+
60+ if ( sources . length > 0 ) {
61+ content += `## Sources\n\n` ;
62+ sources . forEach ( ( source , index ) => {
63+ content += `${ index + 1 } . [${ source . web . title || new URL ( source . web . uri ) . hostname } ](${ source . web . uri } )\n` ;
64+ } ) ;
65+ }
66+
67+ const blob = new Blob ( [ content ] , { type : 'text/markdown' } ) ;
68+ const url = URL . createObjectURL ( blob ) ;
69+ const a = document . createElement ( 'a' ) ;
70+ a . href = url ;
71+ a . download = filename ;
72+ document . body . appendChild ( a ) ;
73+ a . click ( ) ;
74+ document . body . removeChild ( a ) ;
75+ URL . revokeObjectURL ( url ) ;
76+ } ;
77+
5278 if ( isLoading ) {
5379 return (
5480 < div className = "mt-6 p-6 bg-slate-800/50 backdrop-blur-md rounded-lg shadow-lg border border-gray-600/50 flex flex-col items-center justify-center min-h-[20rem]" >
@@ -61,9 +87,9 @@ export const ResultsDisplay: React.FC<ResultsDisplayProps> = ({ text, sources, i
6187
6288 if ( error ) {
6389 return (
64- < div className = "mt -6 p -6 bg-red-900/50 rounded-lg border border-red-500/50" >
90+ < div className = "p -6 mt -6 border rounded-lg bg-red-900/50 border-red-500/50" >
6591 < h3 className = "text-lg font-bold text-red-300" > An Error Occurred</ h3 >
66- < p className = "text-red-200 mt-2 " > { error } </ p >
92+ < p className = "mt-2 text-red-200" > { error } </ p >
6793 </ div >
6894 ) ;
6995 }
@@ -75,22 +101,32 @@ export const ResultsDisplay: React.FC<ResultsDisplayProps> = ({ text, sources, i
75101 </ div >
76102 ) ;
77103 }
78-
104+
79105 const contentLines = text . split ( '\n' ) ;
80106
81107 return (
82- < div className = "mt-6 p-6 bg-slate-800/50 backdrop-blur-md rounded-lg shadow-lg border border-gray-600/50" >
108+ < div className = "p-6 mt-6 border rounded-lg shadow-lg bg-slate-800/50 backdrop-blur-md border-gray-600/50" >
109+ < div className = "flex items-center justify-between mb-4" >
110+ < h2 className = "text-2xl font-bold" > Research Findings</ h2 >
111+ < button
112+ onClick = { handleDownload }
113+ className = "flex items-center gap-2 px-4 py-2 text-sm font-medium text-white transition duration-200 bg-teal-600 rounded-md hover:bg-teal-500 focus:outline-none focus:ring-2 focus:ring-teal-500"
114+ title = "Download research findings as Markdown"
115+ >
116+ < DownloadIcon />
117+ Download
118+ </ button >
119+ </ div >
83120 < div className = "prose prose-invert prose-p:my-0 prose-headings:text-yellow-400 prose-a:text-teal-400 hover:prose-a:text-teal-300 prose-strong:text-gray-100 max-w-none" >
84- < h2 className = "text-2xl font-bold mb-4" > Research Findings</ h2 >
85121 { contentLines . map ( ( line , index ) => (
86122 < ResultItem key = { index } line = { line } onResearchDeeper = { onResearchDeeper } />
87123 ) ) }
88124 </ div >
89125
90126 { sources . length > 0 && (
91- < div className = "mt-8 pt-4 border-t border-gray-600/50" >
92- < h3 className = "text-xl font-bold mb-4 text-yellow-400" > Sources</ h3 >
93- < div className = "grid grid-cols-1 md:grid-cols-2 gap-3 " >
127+ < div className = "pt-4 mt-8 border-t border-gray-600/50" >
128+ < h3 className = "mb-4 text-xl font-bold text-yellow-400" > Sources</ h3 >
129+ < div className = "grid grid-cols-1 gap-3 md:grid-cols-2" >
94130 { sources . map ( ( source , index ) => (
95131 < SourceLink key = { `${ source . web . uri } -${ index } ` } source = { source } />
96132 ) ) }
0 commit comments