Skip to content

Commit

Permalink
1. Add 01ai and doubao ai summary feature && remove claude ai summary…
Browse files Browse the repository at this point in the history
… feature.

2. Remove Inoreader and Raindrop Integration.
3. Add streaming output functionality for LLM topic summary
  • Loading branch information
real-jiakai committed Oct 16, 2024
1 parent f1604cc commit 27d4b07
Show file tree
Hide file tree
Showing 24 changed files with 335 additions and 215 deletions.
131 changes: 88 additions & 43 deletions components/AISummary/index.jsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,106 @@
import { useState, useEffect } from 'react'
import React, { useState, useEffect, useMemo, useCallback } from 'react'
import { CopyToClipboard } from 'react-copy-to-clipboard'

export default function AISummary({ contentMarkdown, params, tags }) {
const [summary, setSummary] = useState('')
const [isCopied, setIsCopied] = useState(false)
const [isFetching, setIsFetching] = useState(false)
const [selectedAI, setSelectedAI] = useState('chatgpt')
const formattedTags = tags.map(tag => `#${tag}`).join(', ')
const [error, setError] = useState('')

const formattedTags = useMemo(() => tags.map(tag => `#${tag}`).join(', '), [tags])

const extractThemeContent = useCallback((content) => {
const themeStart = content.indexOf('## 话题')
const possibleEnds = ['## 有趣', '## 链享', '##言论']
const themeEnd = possibleEnds.reduce((minIndex, endMark) => {
const index = content.indexOf(endMark)
return (index !== -1 && (index < minIndex || minIndex === -1)) ? index : minIndex
}, -1)

if (themeStart !== -1 && themeEnd !== -1) {
return content.slice(themeStart, themeEnd).trim()
}
return ''
}, [])

useEffect(() => {
// 每次 selectedAI 改变时,重置 summary
setSummary('')
setIsCopied(false)
setError('')
}, [selectedAI])

const fetchSummary = () => {
const fetchSummary = useCallback(() => {
setIsFetching(true)
let truncatedContentMarkdown = contentMarkdown.slice(0, 3000)
truncatedContentMarkdown = truncatedContentMarkdown.replace(/"/g, '\\"') // 对双引号进行转义
const message = `using Chinese to summary this article. Below is the article content: \n"${truncatedContentMarkdown}". \nPlease summary this article within 50 chinese words.`
setError('')
setSummary('')
const themeContent = extractThemeContent(contentMarkdown)
const message = `请用中文总结以下主题内容,并在总结末尾提及作者还分享了一些有趣的网站、实用的链接和金句。总结控制在50个中文字以内:\n\n${themeContent}`

fetch(`/api/${selectedAI}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: message,
}),
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
}).then(response => {
const reader = response.body.getReader()
const decoder = new TextDecoder()

function readStream() {
reader.read().then(({ done, value }) => {
if (done) {
setIsFetching(false)
return
}

const chunk = decoder.decode(value)
const lines = chunk.split('\n\n')
lines.forEach(line => {
if (line.startsWith('data: ')) {
const content = line.slice(6).trim()
try {
const data = JSON.parse(content)
if (data.content) {
setSummary(prev => prev + data.content)
}
} catch (error) {
console.error('Error parsing JSON:', error)
}
}
})

readStream()
})
}

readStream()
}).catch(error => {
console.error('Error fetching summary:', error)
setError('获取摘要时出错,请稍后重试。')
setIsFetching(false)
})
.then(response => response.json())
.then(data => {
setSummary(data.summary) // 假设 API 直接返回 summary 字段
setIsFetching(false)
})
.catch(error => {
console.error('Error fetching summary:', error)
setIsFetching(false)
})
}
}, [selectedAI, contentMarkdown, extractThemeContent])

const copyText = `标签:${formattedTags}\n总结: ${summary}\nvia: ${process.env.NEXT_PUBLIC_SITE_URL}/${params.year}/${params.month}/${params.slug}`
const copyText = useMemo(() => `标签:${formattedTags}\n总结: ${summary}\nvia: ${process.env.NEXT_PUBLIC_SITE_URL}/${params.year}/${params.month}/${params.slug}`, [formattedTags, summary, params])

return (
<div className="border border-gray-300 rounded mb-4 relative">
<div className="font-bold mb-2 text-center">文章摘要生成器</div>

{/* 下拉框选择AI */}
<div className="mb-4 text-center">
<select
id="ai-selector"
name="ai-selector"
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md shadow-sm bg-white dark:bg-gray-800 dark:text-white"
value={selectedAI}
onChange={(e) => setSelectedAI(e.target.value)}
aria-label="选择 AI 模型"
>
<option value="chatgpt">GPT-4o-Mini</option>
<option value="claude">Claude-3-Haiku</option>
<option value="lingyiwangwu">Yi-Lightning</option>
<option value="doubao">Doubao-lite-4k</option>
</select>
</div>

{/* 摘要和复制按钮 */}
{summary && (
<div className='bg-white rounded-xl shadow-md p-4 hover:bg-gray-100 transition cursor-copy border'>
<p className="break-words max-w-full">标签:{formattedTags}</p>
Expand All @@ -68,26 +109,30 @@ export default function AISummary({ contentMarkdown, params, tags }) {
</div>
)}

{error && <div className="text-red-500 text-center mb-2">{error}</div>}

<div className="flex justify-center items-center mt-4">
{isFetching ? (
<p className="text-sm">正在生成摘要,请稍候...</p>
) : (
summary ? (
<CopyToClipboard text={copyText} onCopy={() => setIsCopied(true)}>
<button className="bg-blue-500 hover:bg-blue-700 text-white text-xs font-bold py-1 px-2 rounded">
{isCopied ? '已复制' : '复制摘要'}
</button>
</CopyToClipboard>
) : (
<button
className="bg-black hover:bg-gray-700 text-white text-xs font-bold py-1 px-2 rounded"
onClick={fetchSummary}
{!isFetching && !summary && (
<button
className="bg-black hover:bg-gray-700 text-white text-xs font-bold py-1 px-2 rounded"
onClick={fetchSummary}
disabled={isFetching}
aria-label="生成摘要"
>
生成摘要
</button>
)}
{summary && (
<CopyToClipboard text={copyText} onCopy={() => setIsCopied(true)}>
<button
className="bg-blue-500 hover:bg-blue-700 text-white text-xs font-bold py-1 px-2 rounded"
aria-label={isCopied ? '已复制摘要' : '复制摘要'}
>
生成摘要
{isCopied ? '已复制' : '复制摘要'}
</button>
)
</CopyToClipboard>
)}
</div>
</div>
)
}
}
8 changes: 4 additions & 4 deletions components/RightSidebar/index.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import RecentPosts from 'components/RecentPosts'
import Inoreader from 'components/Inoreader'
import Raindrop from 'components/Raindrop'
// import Inoreader from 'components/Inoreader'
// import Raindrop from 'components/Raindrop'
import TagCloud from 'components/TagCloud'

export default function RightSidebar({ recentPosts, allTags}) {
return (
<>
<RecentPosts posts={recentPosts} />
{ process.env.NEXT_PUBLIC_INOREADER_CHANNEL && <Inoreader />}
{ process.env.NEXT_PUBLIC_RAINDROP && <Raindrop />}
{/* { process.env.NEXT_PUBLIC_INOREADER_CHANNEL && <Inoreader />} */}
{/* { process.env.NEXT_PUBLIC_RAINDROP && <Raindrop />} */}
<TagCloud tags={allTags} />
</>
)
Expand Down
36 changes: 26 additions & 10 deletions pages/api/chatgpt.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,36 @@ const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
})

// Create a completion
export default async function handler(req, res) {
if (req.method === 'POST') {
const { message } = req.body

const completion = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{
role: 'user',
content: message,
}],
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
})

const summary = completion.choices[0].message.content
res.status(200).json({ summary })
try {
const stream = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: message }],
stream: true,
})

for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || ''
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`)
}
}
} catch (error) {
console.error('Error in OpenAI stream:', error)
res.write(`data: ${JSON.stringify({ error: '获取摘要时出错,请稍后重试。' })}\n\n`)
} finally {
res.end()
}
} else {
res.status(405).json({ error: 'Method not allowed' })
}
}
}
21 changes: 0 additions & 21 deletions pages/api/claude.js

This file was deleted.

40 changes: 40 additions & 0 deletions pages/api/doubao.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import OpenAI from 'openai'

const openai = new OpenAI({
baseURL: process.env.DOUBAO_BASE_URL,
apiKey: process.env.DOUBAO_API_KEY
})

export default async function handler(req, res) {
if (req.method === 'POST') {
const { message } = req.body

res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
})

try {
const stream = await openai.chat.completions.create({
model: 'Doubao-lite-4k',
messages: [{ role: 'user', content: message }],
stream: true,
})

for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || ''
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`)
}
}
} catch (error) {
console.error('Error in OpenAI stream:', error)
res.write(`data: ${JSON.stringify({ error: '获取摘要时出错,请稍后重试。' })}\n\n`)
} finally {
res.end()
}
} else {
res.status(405).json({ error: 'Method not allowed' })
}
}
40 changes: 40 additions & 0 deletions pages/api/lingyiwangwu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import OpenAI from 'openai'

const openai = new OpenAI({
baseURL: process.env.LINGYIWANGWU_BASE_URL,
apiKey: process.env.LINGYIWANGWU_API_KEY
})

export default async function handler(req, res) {
if (req.method === 'POST') {
const { message } = req.body

res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
})

try {
const stream = await openai.chat.completions.create({
model: 'yi-lightning',
messages: [{ role: 'user', content: message }],
stream: true,
})

for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || ''
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`)
}
}
} catch (error) {
console.error('Error in OpenAI stream:', error)
res.write(`data: ${JSON.stringify({ error: '获取摘要时出错,请稍后重试。' })}\n\n`)
} finally {
res.end()
}
} else {
res.status(405).json({ error: 'Method not allowed' })
}
}
2 changes: 1 addition & 1 deletion posts/AI情感伴侣选择谁?-21.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ mutual love/相思相爱

该歌为电影《名侦探柯南:百万美元的五棱星》的主题曲。中秋节出去玩耍顺带看了这部电影,最后平次的告白,和叶竟然没听见😆。

## 主题:AI情感伴侣选择谁?
## 话题:AI情感伴侣选择谁?

北京时间9月25日清晨,我满怀期待地打开手机,果然GPT Advcanced Voice Mode出来了,原本睡眼惺忪的我,突然来了精神,立马起床出宿舍,体验惺惺念念的这个voice功能。

Expand Down
2 changes: 1 addition & 1 deletion posts/今天我学了什么-15.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ audio:



## 主题:今天我学了什么
## 话题:今天我学了什么

**最快的学习方式是将自己的学习过程公开**。Learn In Public的布道师 swyx在其博文[Learn In Public](https://www.swyx.io/learn-in-public)中提及了这个观点。

Expand Down
2 changes: 1 addition & 1 deletion posts/你好2023-11.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ audio:

<div id="aplayer"></div>

## 本期话题:你好2023
## 话题:你好2023

<iframe src="https://player.bilibili.com/player.html?aid=519647638&bvid=BV1Wg411x7sx&cid=948309604&page=1&autoplay=0" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" class="bilibili"> </iframe>

Expand Down
Loading

0 comments on commit 27d4b07

Please sign in to comment.