/** * @license * SPDX-License-Identifier: Apache-2.0 */ import { render } from 'preact'; import { useState, useEffect, useRef } from 'preact/hooks'; import { html } from 'htm/preact'; import { marked } from 'marked'; declare const pdfjsLib: any; const API_URL = 'https://infomationyc-infomationyc-rltzocirpl.cn-beijing.fcapp.run'; function App() { // UI State const [isLoading, setIsLoading] = useState(false); // For AI backend calls const [isRenderingPdf, setIsRenderingPdf] = useState(false); // For client-side PDF rendering const [pdfViewerVisible, setPdfViewerVisible] = useState(false); // Data State const [fileName, setFileName] = useState(''); const [inputText, setInputText] = useState(''); const [outputText, setOutputText] = useState(''); const [chatHistory, setChatHistory] = useState<{ role: 'user' | 'model'; content: string }[]>([]); const [chatInput, setChatInput] = useState(''); const [isChatting, setIsChatting] = useState(false); // Refs const fileInputRef = useRef(null); const chatHistoryRef = useRef(null); const pdfViewerRef = useRef(null); // Derived State const isBusy = isLoading || isRenderingPdf; // Scroll chat to bottom useEffect(() => { if (chatHistoryRef.current) { chatHistoryRef.current.scrollTop = chatHistoryRef.current.scrollHeight; } }, [chatHistory]); // Add copy listener to PDF viewer useEffect(() => { const viewer = pdfViewerRef.current; if (!viewer) return; const handleCopy = () => { const selection = document.getSelection()?.toString().trim(); if (selection) { setInputText(prev => prev ? `${prev}\n\n${selection}` : selection); const mainTextarea = document.getElementById('main-textarea') as HTMLTextAreaElement; if(mainTextarea) { mainTextarea.focus(); setTimeout(() => { mainTextarea.scrollTop = mainTextarea.scrollHeight; }, 0); } } }; viewer.addEventListener('copy', handleCopy); return () => viewer.removeEventListener('copy', handleCopy); }, [pdfViewerVisible]); const handleFileChange = async (event: Event) => { const file = (event.target as HTMLInputElement).files?.[0]; if (file && file.type === 'application/pdf') { setFileName(file.name); setIsRenderingPdf(true); setPdfViewerVisible(true); const viewer = pdfViewerRef.current; if (viewer) viewer.innerHTML = ''; // Clear previous PDF try { const arrayBuffer = await file.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const viewport = page.getViewport({ scale: 1.5 }); const pageContainer = document.createElement('div'); pageContainer.className = 'page-container'; pageContainer.style.width = `${viewport.width}px`; pageContainer.style.height = `${viewport.height}px`; const canvas = document.createElement('canvas'); const canvasContext = canvas.getContext('2d')!; canvas.height = viewport.height; canvas.width = viewport.width; const textLayerDiv = document.createElement('div'); textLayerDiv.className = 'textLayer'; pageContainer.appendChild(canvas); pageContainer.appendChild(textLayerDiv); if (viewer) viewer.appendChild(pageContainer); await page.render({ canvasContext, viewport }).promise; const textContent = await page.getTextContent(); pdfjsLib.renderTextLayer({ textContentSource: textContent, container: textLayerDiv, viewport: viewport, textDivs: [] }); } } catch (error) { console.error('Error processing PDF:', error); alert('处理PDF文件时出错。'); closePdf(); } finally { setIsRenderingPdf(false); } } else if (file) { alert('请选择一个PDF文件。'); } }; const handleBackendStream = async (requestBody: object, target: 'output' | 'chat') => { setIsLoading(true); let fullResponse = ''; if (target === 'chat') { setIsChatting(true); setChatHistory(prev => [...prev, { role: 'model' as const, content: '' }]); } else { setOutputText(''); } try { const response = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody), }); if (!response.ok || !response.body) { throw new Error(`后端请求失败: ${response.status} ${response.statusText}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; fullResponse += decoder.decode(value, { stream: true }); if (target === 'chat') { setChatHistory(prev => { const newHistory = [...prev]; newHistory[newHistory.length - 1].content = fullResponse; return newHistory; }); } else { setOutputText(fullResponse); } } } catch (error) { const errorMessage = `与后端通信时发生错误: ${error instanceof Error ? error.message : String(error)}`; if (target === 'chat') { setChatHistory(prev => { const newHistory = [...prev]; newHistory[newHistory.length - 1].content = errorMessage; return newHistory; }); } else { setOutputText(errorMessage); } } finally { setIsLoading(false); if (target === 'chat') { setIsChatting(false); } } }; const handlePolishText = () => { if (!inputText.trim()) { alert('请输入需要修改的文本。'); return; } const prompt = `请仔细修改以下文本,修正其中的错别字、中文语法错误、逻辑问题和不通顺的表达,使其表述更清晰、专业。请直接返回修改后的最终文本,不要添加任何额外的解释或前缀文字。\n\n---\n\n${inputText}`; handleBackendStream({ mode: 'text', prompt }, 'output'); }; const handleOrganizeNotes = () => { if (!inputText.trim()) { alert('请输入需要整理的笔记内容。'); return; } const prompt = `请根据以下笔记内容进行整理和概括,生成一份结构清晰、重点突出的笔记。请在最终输出中,首先展示整理后的笔记,然后附上原始笔记内容以供对照。\n\n请使用以下格式输出:\n\n### 整理后\n[这里是你的整理和概括]\n\n---\n\n### 原始笔记\n[这里是原始笔记内容]\n\n---\n\n**原始笔记内容:**\n\n${inputText}`; handleBackendStream({ mode: 'text', prompt }, 'output'); }; const handleChatSubmit = async (e: Event) => { e.preventDefault(); if (!chatInput.trim() || isBusy) return; const newUserMessage = chatInput.trim(); const context = outputText ? `基于以下上下文信息:\n\n---\n${outputText}\n---\n\n` : ''; const updatedHistory = [...chatHistory, { role: 'user' as const, content: newUserMessage }]; setChatHistory(updatedHistory); setChatInput(''); await handleBackendStream({ mode: 'chat', history: updatedHistory, prompt: `${context}${newUserMessage}` }, 'chat'); }; const exportOutputAsTxt = () => { if (!outputText) return; const blob = new Blob([outputText], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `笔记导出-${new Date().toISOString().slice(0,10)}.txt`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; const closePdf = () => { setPdfViewerVisible(false); setFileName(''); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; const renderFormattedText = (content: string) => { const rawHtml = marked.parse(content); return html`
`; }; return html`
${pdfViewerVisible && html`
${fileName}
${isRenderingPdf && html`
正在渲染PDF,请稍候...
`}
`}

智能写作与笔记助手

AI处理结果
${isLoading && !isChatting ? 'AI正在处理中,请稍候...' : (outputText ? renderFormattedText(outputText) : html`...`)}

AI 对话

${chatHistory.length > 0 ? chatHistory.map(msg => html`
${renderFormattedText(msg.content)}
`) : html`
可以就上方生成的内容进行追问...
`}
setChatInput(e.target.value)} disabled=${isBusy} />
`; } render(html`<${App} />`, document.getElementById('app')!);