mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-28 09:21:38 +08:00
🐛 fix(ai): 修正 SQL 代码块 Markdown 换行渲染
- 为 AI markdown 渲染补充 fenced code block 预处理 - 修正 opening/closing fence 缺少换行时的代码块解析失败 - 补充回归测试并更新 issue backlog 记录 Fixes #369
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
| #349 | [Bug] postgres对于表名大小写敏感,且为大写时,通过选中表右键新建查询时生成的sql语句没有自动带上引号"" | Fixed | Pending |
|
||||
| #363 | [Bug] 日期字段无法设置值 | Fixed | Pending |
|
||||
| #368 | [Bug] 窗口状态问题 | Fixed | Pending |
|
||||
| #369 | [Bug] AI回复Sql语句时 md 没有正常渲染 | Fixed | Pending |
|
||||
| #351 | 为什么没有截断和清空表的功能呀? | Fixed | Pending |
|
||||
|
||||
## Notes
|
||||
@@ -158,6 +159,12 @@
|
||||
- 处理:抽出 `windowStateUi` 规则 helper,将“最大化窗口的 scale-fix toggle”收敛为仅在 `ratio-change` 且确实存在 drift 时才执行;激活返回时只广播 `resize`,不再重做最大化动画。并让标题栏按钮根据 `windowState` 动态切换 maximize/restore 图标,同时在标题栏切换动作后立即同步 store 中的窗口状态。
|
||||
- 验证:新增 `frontend/src/utils/windowStateUi.test.ts`,覆盖“activation 不应重 toggle 最大化窗口”和“maximized 状态切换为 restore 图标”两条规则,并执行 `frontend` 下 `npm exec vitest run src/utils/windowStateUi.test.ts` 与 `npm run build`。
|
||||
|
||||
### #369
|
||||
|
||||
- 根因:AI 消息 markdown 渲染链路把模型返回内容原样交给 `react-markdown`,没有对损坏的 fenced code block 做任何预处理。当模型输出 ` ```sqlSELECT ...` 这类“语言标记后缺少换行”的内容时,markdown 解析器会把整段当普通文本/行内代码处理,导致 SQL 代码块和换行都失效。
|
||||
- 处理:新增 `aiMarkdown` 预处理 helper,在渲染前补齐 opening fence 后缺失的换行,并为 closing fence 缺少前置换行的场景补齐收尾;`AIMessageBubble` 统一使用规范化后的内容喂给 `react-markdown`,恢复 SQL/代码块的正常渲染。
|
||||
- 验证:新增 `frontend/src/utils/aiMarkdown.test.ts`,覆盖 ` ```sqlSELECT ...` 自动归一化为 ` ```sql\\nSELECT ...` 的坏样例,并执行 `frontend` 下 `npm exec vitest run src/utils/aiMarkdown.test.ts` 与 `npm run build`。
|
||||
|
||||
### #330
|
||||
|
||||
- 根因:查询结果表格已经支持拖拽调整列宽,但 resize handle 没有提供双击自适应逻辑,导致用户只能靠手工拖拽慢慢试宽度。
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus, vs } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { AIChatMessage, AIToolCall } from '../../types';
|
||||
import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
|
||||
import { normalizeAiMarkdown } from '../../utils/aiMarkdown';
|
||||
|
||||
// 🔧 性能优化:将 ReactMarkdown 包装为 Memo 组件并提取固定的 plugins
|
||||
const remarkPlugins = [remarkGfm];
|
||||
@@ -27,6 +28,7 @@ const MemoizedMarkdown = React.memo(({
|
||||
activeConnectionId?: string;
|
||||
activeDbName?: string;
|
||||
}) => {
|
||||
const normalizedContent = React.useMemo(() => normalizeAiMarkdown(content), [content]);
|
||||
// 缓存 components 对象,避免每次渲染都生成新的函数引用击穿内部子组件的 memo
|
||||
const components = React.useMemo(() => ({
|
||||
code({ node, inline, className, children, ...props }: any) {
|
||||
@@ -46,7 +48,7 @@ const MemoizedMarkdown = React.memo(({
|
||||
|
||||
return (
|
||||
<ReactMarkdown remarkPlugins={remarkPlugins} components={components}>
|
||||
{content}
|
||||
{normalizedContent}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
});
|
||||
|
||||
11
frontend/src/utils/aiMarkdown.test.ts
Normal file
11
frontend/src/utils/aiMarkdown.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { normalizeAiMarkdown } from './aiMarkdown';
|
||||
|
||||
describe('normalizeAiMarkdown', () => {
|
||||
it('inserts a missing newline after the fenced code language marker', () => {
|
||||
expect(normalizeAiMarkdown('```sqlSELECT COUNT(*) AS order_count\nFROM customer_order;\n```')).toBe(
|
||||
'```sql\nSELECT COUNT(*) AS order_count\nFROM customer_order;\n```',
|
||||
);
|
||||
});
|
||||
});
|
||||
13
frontend/src/utils/aiMarkdown.ts
Normal file
13
frontend/src/utils/aiMarkdown.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const normalizeAiMarkdown = (content: string): string => {
|
||||
let text = String(content || '').replace(/\r\n/g, '\n');
|
||||
const knownFenceLanguages = [
|
||||
'sql', 'mermaid', 'json', 'javascript', 'typescript', 'ts', 'js', 'tsx', 'jsx',
|
||||
'bash', 'sh', 'shell', 'python', 'py', 'go', 'java', 'yaml', 'yml', 'html', 'css',
|
||||
'xml', 'markdown', 'md', 'text', 'plaintext', 'vue', 'php', 'ruby', 'rust', 'toml',
|
||||
'ini', 'diff',
|
||||
];
|
||||
const fencePattern = new RegExp(`(^|\\n)\`\`\`(${knownFenceLanguages.join('|')})([^\\n])`, 'gi');
|
||||
text = text.replace(fencePattern, '$1```$2\n$3');
|
||||
text = text.replace(/([^\n])```(?=\n|$)/g, '$1\n```');
|
||||
return text;
|
||||
};
|
||||
Reference in New Issue
Block a user