feat: integrate notion-markdown for markdown conversion in NotionHistoryConnector

- Added notion-markdown dependency to pyproject.toml.
- Refactored _markdown_to_blocks method to utilize notion-markdown for converting markdown content to Notion blocks.
- Updated create-notion-page component to replace Loader2Icon with Spinner for improved loading indication.
This commit is contained in:
Anish Sarkar 2026-03-17 23:42:26 +05:30
parent 39ce597907
commit 0b8bee0076
4 changed files with 4254 additions and 4313 deletions

View file

@ -6,6 +6,7 @@ from collections.abc import Awaitable, Callable
from typing import Any, TypeVar from typing import Any, TypeVar
from notion_client import AsyncClient from notion_client import AsyncClient
from notion_markdown import to_notion
from notion_client.errors import APIResponseError from notion_client.errors import APIResponseError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select from sqlalchemy.future import select
@ -834,106 +835,8 @@ class NotionHistoryConnector:
return None return None
def _markdown_to_blocks(self, markdown: str) -> list[dict[str, Any]]: def _markdown_to_blocks(self, markdown: str) -> list[dict[str, Any]]:
""" """Convert markdown content to Notion blocks using notion-markdown."""
Convert markdown content to Notion blocks. return to_notion(markdown)
This is a simple converter that handles basic markdown.
For more complex markdown, consider using a proper markdown parser.
Args:
markdown: Markdown content
Returns:
List of Notion block objects
"""
blocks = []
lines = markdown.split("\n")
for line in lines:
line = line.strip()
if not line:
continue
# Heading 1
if line.startswith("# "):
blocks.append(
{
"object": "block",
"type": "heading_1",
"heading_1": {
"rich_text": [
{"type": "text", "text": {"content": line[2:]}}
]
},
}
)
# Heading 2
elif line.startswith("## "):
blocks.append(
{
"object": "block",
"type": "heading_2",
"heading_2": {
"rich_text": [
{"type": "text", "text": {"content": line[3:]}}
]
},
}
)
# Heading 3
elif line.startswith("### "):
blocks.append(
{
"object": "block",
"type": "heading_3",
"heading_3": {
"rich_text": [
{"type": "text", "text": {"content": line[4:]}}
]
},
}
)
# Bullet list
elif line.startswith("- ") or line.startswith("* "):
blocks.append(
{
"object": "block",
"type": "bulleted_list_item",
"bulleted_list_item": {
"rich_text": [
{"type": "text", "text": {"content": line[2:]}}
]
},
}
)
# Numbered list
elif match := re.match(r"^(\d+)\.\s+(.*)$", line):
content = match.group(2) # Extract text after "number. "
blocks.append(
{
"object": "block",
"type": "numbered_list_item",
"numbered_list_item": {
"rich_text": [
{"type": "text", "text": {"content": content}}
]
},
}
)
# Regular paragraph
else:
blocks.append(
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [{"type": "text", "text": {"content": line}}]
},
}
)
return blocks
async def create_page( async def create_page(
self, title: str, content: str, parent_page_id: str | None = None self, title: str, content: str, parent_page_id: str | None = None

View file

@ -68,6 +68,7 @@ dependencies = [
"deepagents>=0.4.3", "deepagents>=0.4.3",
"langchain-daytona>=0.0.2", "langchain-daytona>=0.0.2",
"pypandoc>=1.16.2", "pypandoc>=1.16.2",
"notion-markdown>=0.7.0",
] ]
[dependency-groups] [dependency-groups]

8420
surfsense_backend/uv.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { makeAssistantToolUI } from "@assistant-ui/react"; import { makeAssistantToolUI } from "@assistant-ui/react";
import { CornerDownLeftIcon, Loader2Icon, Pen } from "lucide-react"; import { CornerDownLeftIcon, Pen } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -12,6 +12,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { PlateEditor } from "@/components/editor/plate-editor"; import { PlateEditor } from "@/components/editor/plate-editor";
import { Spinner } from "@/components/ui/spinner";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom"; import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
@ -288,31 +289,25 @@ function ApprovalCard({
{/* Content preview */} {/* Content preview */}
<div className="mx-5 h-px bg-border/50" /> <div className="mx-5 h-px bg-border/50" />
<div className="px-5 pt-4 space-y-3"> <div className="px-5 pt-3">
{args.title != null && ( {args.title != null && (
<div> <p className="text-sm font-medium text-foreground">{String(args.title)}</p>
<p className="text-xs font-medium text-muted-foreground">Title</p>
<p className="mt-0.5 text-sm text-foreground">{String(args.title)}</p>
</div>
)} )}
{args.content != null && ( {args.content != null && (
<div> <div
<p className="text-xs font-medium text-muted-foreground">Content</p> className="max-h-[7rem] overflow-hidden text-sm"
<div style={{
className="mt-0.5 max-h-[7rem] overflow-hidden text-sm" maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
style={{ WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)", }}
WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)", >
}} <PlateEditor
> markdown={String(args.content)}
<PlateEditor readOnly
markdown={String(args.content)} preset="readonly"
readOnly editorVariant="none"
preset="readonly" className="h-auto [&_[data-slate-editor]]:!min-h-0 [&_[data-slate-editor]>*:first-child]:!mt-0"
editorVariant="none" />
className="h-auto [&_[data-slate-editor]]:!min-h-0"
/>
</div>
</div> </div>
)} )}
</div> </div>
@ -407,7 +402,7 @@ export const CreateNotionPageToolUI = makeAssistantToolUI<
if (status.type === "running") { if (status.type === "running") {
return ( return (
<div className="my-4 flex max-w-lg items-center gap-3 rounded-2xl border bg-muted/30 px-5 py-4"> <div className="my-4 flex max-w-lg items-center gap-3 rounded-2xl border bg-muted/30 px-5 py-4">
<Loader2Icon className="size-4 animate-spin text-muted-foreground" /> <Spinner size="sm" className="text-muted-foreground" />
<p className="text-sm text-muted-foreground">Preparing Notion page...</p> <p className="text-sm text-muted-foreground">Preparing Notion page...</p>
</div> </div>
); );