Compare commits
No commits in common. "2e5120dc724988cd489b8df72c128980dce70b26" and "470e34e7a888db0582bce4648f8236402b37acac" have entirely different histories.
2e5120dc72
...
470e34e7a8
@ -490,7 +490,7 @@ export default function ChatPage({ params }: PageProps) {
|
||||
|
||||
{/* 固定底部输入框 */}
|
||||
<div
|
||||
className="sticky bottom-0 pt-4 z-20"
|
||||
className="sticky bottom-0 pt-4"
|
||||
style={{
|
||||
background: `linear-gradient(to top, var(--color-bg-secondary) 0%, var(--color-bg-secondary) 80%, transparent 100%)`
|
||||
}}
|
||||
|
||||
@ -127,7 +127,7 @@ export function ChatInput({
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
placeholder={files.length > 0 ? '添加描述(可选)...' : placeholder}
|
||||
className="w-full border-none outline-none text-[var(--color-text-primary)] bg-transparent py-2 placeholder:text-[var(--color-text-placeholder)]"
|
||||
className="w-full border-none outline-none text-base text-[var(--color-text-primary)] bg-transparent py-2 placeholder:text-[var(--color-text-placeholder)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -152,7 +152,7 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-[var(--color-message-user)] text-[var(--color-text-primary)] px-4 py-3 rounded-md leading-relaxed">
|
||||
<div className="bg-[var(--color-message-user)] text-[var(--color-text-primary)] px-4 py-3 rounded-md text-base leading-relaxed">
|
||||
{message.content || ((uploadedImages && uploadedImages.length > 0) || (uploadedDocuments && uploadedDocuments.length > 0) ? '(附件)' : '')}
|
||||
</div>
|
||||
{/* 悬停显示复制按钮 */}
|
||||
@ -216,7 +216,7 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
|
||||
</button>
|
||||
{thinkingExpanded && (
|
||||
<div className="mt-2 p-4 bg-purple-50 border border-purple-200 rounded-lg">
|
||||
<pre className="text-purple-800 whitespace-pre-wrap font-mono">
|
||||
<pre className="text-sm text-purple-800 whitespace-pre-wrap font-mono">
|
||||
{thinkingContent}
|
||||
</pre>
|
||||
</div>
|
||||
@ -228,13 +228,13 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
|
||||
{error && (
|
||||
<div className="mb-3 p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-3">
|
||||
<AlertCircle size={20} className="text-red-500 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-red-700">{error}</div>
|
||||
<div className="text-sm text-red-700">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 主要内容 */}
|
||||
<div className="bg-[var(--color-message-assistant-bg)] border border-[var(--color-message-assistant-border)] rounded-md px-5 py-4 shadow-sm">
|
||||
<div className="text-[var(--color-text-primary)] leading-[1.75]">
|
||||
<div className="text-sm text-[var(--color-text-primary)] leading-[1.75]">
|
||||
{message.content ? (
|
||||
<MarkdownRenderer content={message.content} />
|
||||
) : isStreaming ? (
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState, useMemo } from 'react';
|
||||
import { Copy, Check, Eye, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Copy, Check, Eye } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import Prism from 'prismjs';
|
||||
import { HtmlPreviewModal } from '@/components/ui/HtmlPreviewModal';
|
||||
@ -30,10 +30,6 @@ interface CodeBlockProps {
|
||||
language?: string;
|
||||
showLineNumbers?: boolean;
|
||||
className?: string;
|
||||
/** 折叠阈值,超过此行数自动折叠,默认 60 */
|
||||
maxCollapsedLines?: number;
|
||||
/** 折叠时预览的行数,默认 30 */
|
||||
previewLines?: number;
|
||||
}
|
||||
|
||||
// 语言别名映射
|
||||
@ -55,12 +51,9 @@ export function CodeBlock({
|
||||
language = 'text',
|
||||
showLineNumbers = true,
|
||||
className,
|
||||
maxCollapsedLines = 60,
|
||||
previewLines = 30,
|
||||
}: CodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// 规范化语言名称
|
||||
const normalizedLanguage = languageAliases[language.toLowerCase()] || language.toLowerCase();
|
||||
@ -69,33 +62,17 @@ export function CodeBlock({
|
||||
const isHtmlPreviewable = ['html', 'htm', 'markup'].includes(normalizedLanguage) ||
|
||||
['html', 'htm'].includes(language.toLowerCase());
|
||||
|
||||
const lines = code.split('\n');
|
||||
const totalLines = lines.length;
|
||||
const shouldCollapse = totalLines > maxCollapsedLines;
|
||||
const remainingLines = totalLines - previewLines;
|
||||
|
||||
// 根据折叠状态获取显示的代码
|
||||
const displayCode = useMemo(() => {
|
||||
if (shouldCollapse && !isExpanded) {
|
||||
return lines.slice(0, previewLines).join('\n');
|
||||
}
|
||||
return code;
|
||||
}, [code, lines, shouldCollapse, isExpanded, previewLines]);
|
||||
|
||||
const displayLines = displayCode.split('\n');
|
||||
|
||||
// 使用 useMemo 缓存高亮后的 HTML,避免频繁重新高亮
|
||||
const highlightedCode = useMemo(() => {
|
||||
const grammar = Prism.languages[normalizedLanguage];
|
||||
if (grammar) {
|
||||
return Prism.highlight(displayCode, grammar, normalizedLanguage);
|
||||
return Prism.highlight(code, grammar, normalizedLanguage);
|
||||
}
|
||||
return displayCode;
|
||||
}, [displayCode, normalizedLanguage]);
|
||||
return code;
|
||||
}, [code, normalizedLanguage]);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
// 始终复制完整代码
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
@ -104,15 +81,13 @@ export function CodeBlock({
|
||||
}
|
||||
};
|
||||
|
||||
const toggleExpand = () => {
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
const lines = code.split('\n');
|
||||
|
||||
return (
|
||||
<div className={cn('relative group rounded overflow-hidden my-4', className)}
|
||||
style={{ boxShadow: 'var(--color-code-shadow)' }}>
|
||||
{/* 顶部工具栏 */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 text-[0.9em]"
|
||||
<div className="flex items-center justify-between px-4 py-2.5 text-sm"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-code-toolbar-bg)',
|
||||
color: 'var(--color-code-toolbar-text)',
|
||||
@ -131,12 +106,6 @@ export function CodeBlock({
|
||||
style={{ backgroundColor: '#27c93f', boxShadow: '0 0 0 1px rgba(0, 0, 0, 0.1) inset' }} />
|
||||
</div>
|
||||
<span className="font-mono text-[13px]">{language || 'code'}</span>
|
||||
{/* 显示总行数 */}
|
||||
{shouldCollapse && (
|
||||
<span className="text-[12px] opacity-60">
|
||||
({totalLines} 行)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧:操作按钮 */}
|
||||
@ -176,93 +145,40 @@ export function CodeBlock({
|
||||
</div>
|
||||
|
||||
{/* 代码区域 */}
|
||||
<div style={{ backgroundColor: 'var(--color-code-bg)' }}>
|
||||
{/* 代码内容 - 使用 flex 布局 */}
|
||||
<div className="flex overflow-x-auto">
|
||||
{/* 行号列 */}
|
||||
{showLineNumbers && (
|
||||
<div
|
||||
className="flex-shrink-0 w-12 select-none py-4 px-2 text-right"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-code-bg)',
|
||||
borderRight: '1px solid var(--color-code-line-border)'
|
||||
}}
|
||||
>
|
||||
{displayLines.map((_, index) => (
|
||||
<div className="relative overflow-x-auto"
|
||||
style={{ backgroundColor: 'var(--color-code-bg)' }}>
|
||||
{showLineNumbers && (
|
||||
<div className="absolute left-0 top-0 bottom-0 w-12 select-none"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-code-bg)',
|
||||
borderRight: '1px solid var(--color-code-line-border)'
|
||||
}}>
|
||||
<div className="py-4 px-2 text-right">
|
||||
{lines.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="font-mono leading-6"
|
||||
className="text-sm font-mono leading-6"
|
||||
style={{ color: 'var(--color-code-line-number)' }}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* 代码列 */}
|
||||
<pre
|
||||
className="flex-1 p-4 overflow-x-auto"
|
||||
style={{ margin: 0, background: 'transparent' }}
|
||||
>
|
||||
<code
|
||||
className={`language-${normalizedLanguage} leading-6`}
|
||||
style={{ color: 'var(--color-code-text)' }}
|
||||
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* 渐变遮罩和展开按钮(仅在折叠状态下显示) */}
|
||||
{shouldCollapse && !isExpanded && (
|
||||
<>
|
||||
{/* 渐变遮罩 */}
|
||||
<div
|
||||
className="h-16 -mt-16 pointer-events-none relative z-10"
|
||||
style={{
|
||||
background: `linear-gradient(to bottom, transparent, var(--color-code-bg))`
|
||||
}}
|
||||
/>
|
||||
{/* 展开按钮 */}
|
||||
<div
|
||||
className="py-3 text-center cursor-pointer transition-colors hover:opacity-80"
|
||||
style={{ backgroundColor: 'var(--color-code-bg)' }}
|
||||
onClick={toggleExpand}
|
||||
>
|
||||
<button
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-[0.9em] font-medium transition-colors"
|
||||
style={{
|
||||
color: 'var(--color-primary)',
|
||||
backgroundColor: 'var(--color-primary-alpha)'
|
||||
}}
|
||||
>
|
||||
<ChevronDown size={16} />
|
||||
<span>展开剩余 {remainingLines} 行代码</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 收起按钮(仅在展开状态下显示) */}
|
||||
{shouldCollapse && isExpanded && (
|
||||
<div
|
||||
className="py-3 text-center cursor-pointer transition-colors"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-code-bg)',
|
||||
borderTop: '1px solid var(--color-code-line-border)'
|
||||
}}
|
||||
onClick={toggleExpand}
|
||||
>
|
||||
<button
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-[0.9em] font-medium transition-colors"
|
||||
style={{
|
||||
color: 'var(--color-code-toolbar-text)'
|
||||
}}
|
||||
>
|
||||
<ChevronUp size={16} />
|
||||
<span>收起代码</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<pre
|
||||
className={cn(
|
||||
'p-4 overflow-x-auto',
|
||||
showLineNumbers && 'pl-16'
|
||||
)}
|
||||
style={{ margin: 0, background: 'transparent' }}
|
||||
>
|
||||
<code
|
||||
className={`language-${normalizedLanguage} text-sm leading-6`}
|
||||
style={{ color: 'var(--color-code-text)' }}
|
||||
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* HTML 预览模态框 */}
|
||||
|
||||
@ -48,31 +48,31 @@ const markdownComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
// 标题 - 使用相对单位保持与全局字体的比例
|
||||
// 标题
|
||||
h1({ children }: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<h1 className="text-[1.25em] font-bold mt-5 mb-3 text-[var(--color-text-primary)]">
|
||||
<h1 className="text-lg font-bold mt-5 mb-3 text-[var(--color-text-primary)]">
|
||||
{children}
|
||||
</h1>
|
||||
);
|
||||
},
|
||||
h2({ children }: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<h2 className="text-[1.125em] font-bold mt-4 mb-2 text-[var(--color-text-primary)]">
|
||||
<h2 className="text-base font-bold mt-4 mb-2 text-[var(--color-text-primary)]">
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
},
|
||||
h3({ children }: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<h3 className="text-[1em] font-semibold mt-3 mb-2 text-[var(--color-text-primary)]">
|
||||
<h3 className="text-sm font-semibold mt-3 mb-2 text-[var(--color-text-primary)]">
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
},
|
||||
h4({ children }: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<h4 className="text-[1em] font-semibold mt-2 mb-1 text-[var(--color-text-primary)]">
|
||||
<h4 className="text-sm font-semibold mt-2 mb-1 text-[var(--color-text-primary)]">
|
||||
{children}
|
||||
</h4>
|
||||
);
|
||||
@ -95,7 +95,7 @@ const markdownComponents = {
|
||||
},
|
||||
li({ children }: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<li className="leading-relaxed">
|
||||
<li className="leading-relaxed text-sm">
|
||||
{children}
|
||||
</li>
|
||||
);
|
||||
@ -136,7 +136,7 @@ const markdownComponents = {
|
||||
// 引用
|
||||
blockquote({ children }: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<blockquote className="border-l-3 border-[var(--color-primary)] pl-3 my-3 italic text-[var(--color-text-secondary)]">
|
||||
<blockquote className="border-l-3 border-[var(--color-primary)] pl-3 my-3 italic text-sm text-[var(--color-text-secondary)]">
|
||||
{children}
|
||||
</blockquote>
|
||||
);
|
||||
@ -153,7 +153,7 @@ const markdownComponents = {
|
||||
table({ children }: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<div className="overflow-x-auto my-3">
|
||||
<table className="min-w-full border border-[var(--color-border)] rounded-lg overflow-hidden text-[0.9em]">
|
||||
<table className="min-w-full border border-[var(--color-border)] rounded-lg overflow-hidden text-sm">
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
@ -182,14 +182,14 @@ const markdownComponents = {
|
||||
},
|
||||
th({ children }: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<th className="px-3 py-1.5 text-left text-[0.85em] font-semibold text-[var(--color-text-primary)]">
|
||||
<th className="px-3 py-1.5 text-left text-xs font-semibold text-[var(--color-text-primary)]">
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
},
|
||||
td({ children }: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<td className="px-3 py-1.5 text-[0.85em] text-[var(--color-text-secondary)]">
|
||||
<td className="px-3 py-1.5 text-xs text-[var(--color-text-secondary)]">
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
X,
|
||||
Monitor,
|
||||
@ -83,29 +83,6 @@ export function HtmlPreviewModal({
|
||||
// 获取当前设备配置
|
||||
const currentDevice = deviceConfigs.find((d) => d.type === device) || deviceConfigs[0];
|
||||
|
||||
// 处理 HTML 代码,注入 <base target="_blank"> 防止链接影响父窗口
|
||||
const processedHtmlCode = useMemo(() => {
|
||||
const baseTag = '<base target="_blank">';
|
||||
|
||||
// 检查是否已经有 <base> 标签
|
||||
if (/<base\s/i.test(htmlCode)) {
|
||||
return htmlCode;
|
||||
}
|
||||
|
||||
// 尝试在 <head> 标签后注入
|
||||
if (/<head[^>]*>/i.test(htmlCode)) {
|
||||
return htmlCode.replace(/<head[^>]*>/i, `$&\n ${baseTag}`);
|
||||
}
|
||||
|
||||
// 尝试在 <html> 标签后注入
|
||||
if (/<html[^>]*>/i.test(htmlCode)) {
|
||||
return htmlCode.replace(/<html[^>]*>/i, `$&\n <head>\n ${baseTag}\n </head>`);
|
||||
}
|
||||
|
||||
// 如果都没有,在最前面添加
|
||||
return `<head>${baseTag}</head>\n${htmlCode}`;
|
||||
}, [htmlCode]);
|
||||
|
||||
// ESC 关闭 / F11 全屏切换
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
@ -371,7 +348,7 @@ export function HtmlPreviewModal({
|
||||
{/* iframe 预览 */}
|
||||
<iframe
|
||||
key={iframeKey}
|
||||
srcDoc={processedHtmlCode}
|
||||
srcDoc={htmlCode}
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
className="w-full bg-white"
|
||||
style={{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user