ai问片移除think,标题可点击
This commit is contained in:
@@ -74,6 +74,9 @@ function transformToSSE(
|
|||||||
return new ReadableStream({
|
return new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
let buffer = ''; // 缓冲区,用于保存不完整的行
|
let buffer = ''; // 缓冲区,用于保存不完整的行
|
||||||
|
let contentBuffer = ''; // 累积的内容,用于处理跨chunk的thinking标签
|
||||||
|
let inThinkingBlock = false; // 是否在thinking块内
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
@@ -122,9 +125,32 @@ function transformToSSE(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
controller.enqueue(
|
// 累积内容并处理thinking标签
|
||||||
new TextEncoder().encode(`data: ${JSON.stringify({ text })}\n\n`)
|
contentBuffer += text;
|
||||||
);
|
|
||||||
|
// 检查是否进入thinking块
|
||||||
|
if (contentBuffer.includes('<think>')) {
|
||||||
|
inThinkingBlock = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否退出thinking块
|
||||||
|
if (inThinkingBlock && contentBuffer.includes('</think>')) {
|
||||||
|
// 移除thinking块内容
|
||||||
|
contentBuffer = contentBuffer.replace(/<think>[\s\S]*?<\/think>/g, '');
|
||||||
|
inThinkingBlock = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有在不在thinking块内时才输出内容
|
||||||
|
if (!inThinkingBlock) {
|
||||||
|
// 输出非thinking部分的内容
|
||||||
|
const outputText = contentBuffer;
|
||||||
|
if (outputText) {
|
||||||
|
controller.enqueue(
|
||||||
|
new TextEncoder().encode(`data: ${JSON.stringify({ text: outputText })}\n\n`)
|
||||||
|
);
|
||||||
|
contentBuffer = ''; // 清空已输出的内容
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 只在非空数据解析失败时打印错误
|
// 只在非空数据解析失败时打印错误
|
||||||
@@ -153,9 +179,14 @@ function transformToSSE(
|
|||||||
text = json.choices?.[0]?.delta?.content || '';
|
text = json.choices?.[0]?.delta?.content || '';
|
||||||
}
|
}
|
||||||
if (text) {
|
if (text) {
|
||||||
controller.enqueue(
|
contentBuffer += text;
|
||||||
new TextEncoder().encode(`data: ${JSON.stringify({ text })}\n\n`)
|
// 最后清理一次thinking标签
|
||||||
);
|
contentBuffer = contentBuffer.replace(/<think>[\s\S]*?<\/think>/g, '');
|
||||||
|
if (contentBuffer) {
|
||||||
|
controller.enqueue(
|
||||||
|
new TextEncoder().encode(`data: ${JSON.stringify({ text: contentBuffer })}\n\n`)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Parse final buffer error:', e);
|
console.error('Parse final buffer error:', e);
|
||||||
@@ -298,7 +329,10 @@ export async function POST(request: NextRequest) {
|
|||||||
// 非流式响应:等待完整响应后返回JSON
|
// 非流式响应:等待完整响应后返回JSON
|
||||||
const response = result as Response;
|
const response = result as Response;
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const content = data.choices?.[0]?.message?.content || '';
|
let content = data.choices?.[0]?.message?.content || '';
|
||||||
|
|
||||||
|
// 移除thinking标签内容
|
||||||
|
content = content.replace(/<think>[\s\S]*?<\/think>/g, '');
|
||||||
|
|
||||||
return NextResponse.json({ content });
|
return NextResponse.json({ content });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Bot, Loader2, Send, Sparkles, Trash2,X } from 'lucide-react';
|
import { Bot, Loader2, Send, Sparkles, Trash2,X } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
@@ -33,6 +35,8 @@ export default function AIChatPanel({
|
|||||||
useDrawer = false,
|
useDrawer = false,
|
||||||
drawerWidth = 'w-full md:w-[25%]',
|
drawerWidth = 'w-full md:w-[25%]',
|
||||||
}: AIChatPanelProps) {
|
}: AIChatPanelProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
// 使用 useMemo 稳定 storage key,只在实际内容变化时才改变
|
// 使用 useMemo 稳定 storage key,只在实际内容变化时才改变
|
||||||
const storageKey = useMemo(() => {
|
const storageKey = useMemo(() => {
|
||||||
if (context?.title) {
|
if (context?.title) {
|
||||||
@@ -53,6 +57,14 @@ export default function AIChatPanel({
|
|||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
const hasLoadedRef = useRef(false);
|
const hasLoadedRef = useRef(false);
|
||||||
|
|
||||||
|
// 将《》包裹的影视名称转换为链接
|
||||||
|
const convertTitleToLink = (content: string): string => {
|
||||||
|
return content.replace(/《([^》]+)》/g, (match, title) => {
|
||||||
|
const encodedTitle = encodeURIComponent(title);
|
||||||
|
return `[《${title}》](/play?title=${encodedTitle})`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 自动滚动到底部
|
// 自动滚动到底部
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
@@ -424,9 +436,29 @@ export default function AIChatPanel({
|
|||||||
{message.content}
|
{message.content}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className='prose prose-sm max-w-none dark:prose-invert prose-p:my-2 prose-p:leading-relaxed prose-pre:bg-gray-800 prose-pre:text-gray-100 dark:prose-pre:bg-gray-900 prose-code:text-purple-600 dark:prose-code:text-purple-400 prose-code:bg-purple-50 dark:prose-code:bg-purple-900/20 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-strong:text-gray-900 dark:prose-strong:text-white prose-ul:my-2 prose-ol:my-2 prose-li:my-1'>
|
<div className='prose prose-sm max-w-none dark:prose-invert prose-p:my-2 prose-p:leading-relaxed prose-pre:bg-gray-800 prose-pre:text-gray-100 dark:prose-pre:bg-gray-900 prose-code:text-purple-600 dark:prose-code:text-purple-400 prose-code:bg-purple-50 dark:prose-code:bg-purple-900/20 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none prose-a:text-inherit dark:prose-a:text-inherit prose-a:no-underline hover:prose-a:underline prose-strong:text-gray-900 dark:prose-strong:text-white prose-ul:my-2 prose-ol:my-2 prose-li:my-1'>
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm as any]}>
|
<ReactMarkdown
|
||||||
{message.content}
|
remarkPlugins={[remarkGfm as any]}
|
||||||
|
components={{
|
||||||
|
a: ({ node, href, children, ...props }) => {
|
||||||
|
// 如果是内部链接(以 / 开头),使用 Next.js Link
|
||||||
|
if (href?.startsWith('/')) {
|
||||||
|
// 如果当前在 /play 页面且链接也是 /play,不做处理(返回纯文本)
|
||||||
|
if (pathname === '/play' && href.startsWith('/play')) {
|
||||||
|
return <span>{children}</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link href={href} {...props}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 外部链接使用普通 a 标签
|
||||||
|
return <a href={href} target="_blank" rel="noopener noreferrer" {...props}>{children}</a>;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{convertTitleToLink(message.content)}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -610,9 +642,29 @@ export default function AIChatPanel({
|
|||||||
{message.content}
|
{message.content}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className='prose prose-sm max-w-none dark:prose-invert prose-p:my-2 prose-p:leading-relaxed prose-pre:bg-gray-800 prose-pre:text-gray-100 dark:prose-pre:bg-gray-900 prose-code:text-purple-600 dark:prose-code:text-purple-400 prose-code:bg-purple-50 dark:prose-code:bg-purple-900/20 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-strong:text-gray-900 dark:prose-strong:text-white prose-ul:my-2 prose-ol:my-2 prose-li:my-1'>
|
<div className='prose prose-sm max-w-none dark:prose-invert prose-p:my-2 prose-p:leading-relaxed prose-pre:bg-gray-800 prose-pre:text-gray-100 dark:prose-pre:bg-gray-900 prose-code:text-purple-600 dark:prose-code:text-purple-400 prose-code:bg-purple-50 dark:prose-code:bg-purple-900/20 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none prose-a:text-inherit dark:prose-a:text-inherit prose-a:no-underline hover:prose-a:underline prose-strong:text-gray-900 dark:prose-strong:text-white prose-ul:my-2 prose-ol:my-2 prose-li:my-1'>
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm as any]}>
|
<ReactMarkdown
|
||||||
{message.content}
|
remarkPlugins={[remarkGfm as any]}
|
||||||
|
components={{
|
||||||
|
a: ({ node, href, children, ...props }) => {
|
||||||
|
// 如果是内部链接(以 / 开头),使用 Next.js Link
|
||||||
|
if (href?.startsWith('/')) {
|
||||||
|
// 如果当前在 /play 页面且链接也是 /play,不做处理(返回纯文本)
|
||||||
|
if (pathname === '/play' && href.startsWith('/play')) {
|
||||||
|
return <span>{children}</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link href={href} {...props}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 外部链接使用普通 a 标签
|
||||||
|
return <a href={href} target="_blank" rel="noopener noreferrer" {...props}>{children}</a>;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{convertTitleToLink(message.content)}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user