- 新增 web2/ 前端:任务卡看板、风险雷达、决策日志、我的团队、快捷指令 - ChatPanel 增强:风险卡置顶、案卡接入、起诉状向导、语音输入 - 事项看板:点击查看详情弹窗、断点续接(恢复历史对话+matterId) - 保密模式恢复:密码验证后重建内存 session,保留多轮上下文 - 多轮对话:ask/ask-stream/ask-analyze 注入 session 历史,AI 有上下文 - 追问优化:紧扣用户问题,不再偏离主题(社保问社保,工伤问工伤) - 对话区可滚动,输入区固定可见 - 状态机放宽:允许从任意非终态转到 pending_expert - session 存活检查端点,后端重启后自动重建 session - DB migration:风险推送表、案卡表 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
113 lines
3.5 KiB
TypeScript
113 lines
3.5 KiB
TypeScript
import ReactMarkdown from 'react-markdown'
|
|
import remarkGfm from 'remark-gfm'
|
|
import rehypeHighlight from 'rehype-highlight'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
/** Markdown 渲染组件:支持 GFM(表格/列表/删除线)+ 代码高亮。
|
|
|
|
* 用于 AI 回复的富文本展示,替代纯 whitespace-pre-wrap。
|
|
* 流式输出时传入 streaming=true 显示光标。
|
|
*/
|
|
export function Markdown({
|
|
content,
|
|
streaming,
|
|
className,
|
|
}: {
|
|
content: string
|
|
streaming?: boolean
|
|
className?: string
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'prose prose-sm max-w-none dark:prose-invert',
|
|
// 继承父级文字色,不被 prose 默认色覆盖
|
|
'prose-headings:text-foreground prose-p:text-foreground prose-li:text-foreground',
|
|
'prose-strong:text-foreground prose-a:text-primary',
|
|
// 紧凑间距
|
|
'prose-headings:my-2 prose-headings:first:mt-0 prose-p:my-2',
|
|
'prose-ul:my-2 prose-ol:my-2 prose-li:my-0.5',
|
|
'prose-pre:my-3 prose-code:before:content-none prose-code:after:content-none',
|
|
'prose-table:my-3 prose-th:px-2 prose-th:py-1 prose-td:px-2 prose-td:py-1',
|
|
className,
|
|
)}
|
|
>
|
|
<ReactMarkdown
|
|
remarkPlugins={[remarkGfm]}
|
|
rehypePlugins={[rehypeHighlight]}
|
|
components={{
|
|
// 代码块:圆角 + 暗色背景
|
|
pre: ({ children, ...props }) => (
|
|
<pre
|
|
className="rounded-lg bg-slate-900 p-3 text-xs text-slate-100 overflow-x-auto"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</pre>
|
|
),
|
|
// 行内代码:浅灰背景
|
|
code: ({ children, className, ...props }) => {
|
|
const isInline = !className?.includes('language-')
|
|
if (isInline) {
|
|
return (
|
|
<code
|
|
className="rounded bg-muted px-1 py-0.5 text-xs font-mono"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</code>
|
|
)
|
|
}
|
|
return (
|
|
<code className={className} {...props}>
|
|
{children}
|
|
</code>
|
|
)
|
|
},
|
|
// 表格:外层包裹 overflow-x-auto 防止小屏溢出
|
|
table: ({ children, ...props }) => (
|
|
<div className="overflow-x-auto">
|
|
<table
|
|
className="w-full min-w-full border-collapse border text-xs"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</table>
|
|
</div>
|
|
),
|
|
th: ({ children, ...props }) => (
|
|
<th
|
|
className="border bg-muted/50 font-semibold text-left"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</th>
|
|
),
|
|
td: ({ children, ...props }) => (
|
|
<td className="border" {...props}>
|
|
{children}
|
|
</td>
|
|
),
|
|
// 链接:新标签打开
|
|
a: ({ children, href, ...props }) => (
|
|
<a
|
|
href={href}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-primary underline underline-offset-2"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</a>
|
|
),
|
|
}}
|
|
>
|
|
{content + (streaming && content && !content.endsWith('\n') ? '\n' : '')}
|
|
</ReactMarkdown>
|
|
{streaming && (
|
|
<span className="ml-0.5 inline-block h-4 w-0.5 animate-pulse bg-current align-text-bottom" aria-hidden />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|