From 73a1e6eb6c95ef5dc39368943efdb911525d2fcf Mon Sep 17 00:00:00 2001 From: Innei Date: Thu, 31 Jul 2025 18:25:59 +0800 Subject: [PATCH] feat: integrate Lexical rich text editor and update chat components - Added support for rich text messages in the chat interface using Lexical. - Updated ChatInput and related components to handle EditorState and rich text formats. - Introduced AIRichTextMessage component for rendering rich text messages. - Removed legacy LexicalRichEditor component and adjusted database schema to drop richTextSchema. - Updated dependencies to include Lexical packages at version 0.33.1. Signed-off-by: Innei --- .../displays/AIDisplayEntriesPart.tsx | 2 +- .../displays/AIDisplayFeedsPart.tsx | 2 +- .../displays/AIDisplaySubscriptionsPart.tsx | 2 +- .../ai/chat/components/layouts/ChatInput.tsx | 17 +- .../chat/components/layouts/ChatInterface.tsx | 41 +- .../chat/components/layouts/WelcomeScreen.tsx | 7 +- .../components/message/AIMessageParts.tsx | 12 + .../components/message/AIRichTextMessage.tsx | 74 ++ .../src/modules/ai/chat/services/index.ts | 28 - packages/internal/components/package.json | 8 + .../LexicalRichEditor.tsx | 130 +-- .../src/ui/lexical-rich-editor/index.tsx | 4 + .../plugins/KeyboardPlugin.tsx | 29 + .../ui/lexical-rich-editor/plugins/index.ts | 1 + .../src/ui/lexical-rich-editor/theme.ts | 69 ++ .../src/ui/lexical-rich-editor/types.ts | 25 + .../src/drizzle/0034_curly_darkstar.sql | 1 + .../src/drizzle/meta/0034_snapshot.json | 938 ++++++++++++++++++ .../database/src/drizzle/meta/_journal.json | 7 + .../database/src/drizzle/migrations.js | 2 + .../internal/database/src/schemas/index.ts | 7 - pnpm-lock.yaml | 24 + 22 files changed, 1261 insertions(+), 169 deletions(-) create mode 100644 apps/desktop/layer/renderer/src/modules/ai/chat/components/message/AIRichTextMessage.tsx rename {apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts => packages/internal/components/src/ui/lexical-rich-editor}/LexicalRichEditor.tsx (54%) create mode 100644 packages/internal/components/src/ui/lexical-rich-editor/index.tsx create mode 100644 packages/internal/components/src/ui/lexical-rich-editor/plugins/KeyboardPlugin.tsx create mode 100644 packages/internal/components/src/ui/lexical-rich-editor/plugins/index.ts create mode 100644 packages/internal/components/src/ui/lexical-rich-editor/theme.ts create mode 100644 packages/internal/components/src/ui/lexical-rich-editor/types.ts create mode 100644 packages/internal/database/src/drizzle/0034_curly_darkstar.sql create mode 100644 packages/internal/database/src/drizzle/meta/0034_snapshot.json diff --git a/apps/desktop/layer/renderer/src/modules/ai/chat/components/displays/AIDisplayEntriesPart.tsx b/apps/desktop/layer/renderer/src/modules/ai/chat/components/displays/AIDisplayEntriesPart.tsx index 2b27add4c..789a01ff3 100644 --- a/apps/desktop/layer/renderer/src/modules/ai/chat/components/displays/AIDisplayEntriesPart.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai/chat/components/displays/AIDisplayEntriesPart.tsx @@ -343,7 +343,7 @@ export const AIDisplayEntriesPart = ({ part }: { part: AIDisplayEntriesTool }) = return ( -
+
📰 diff --git a/apps/desktop/layer/renderer/src/modules/ai/chat/components/displays/AIDisplayFeedsPart.tsx b/apps/desktop/layer/renderer/src/modules/ai/chat/components/displays/AIDisplayFeedsPart.tsx index f0bdc76fe..202f78752 100644 --- a/apps/desktop/layer/renderer/src/modules/ai/chat/components/displays/AIDisplayFeedsPart.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai/chat/components/displays/AIDisplayFeedsPart.tsx @@ -135,7 +135,7 @@ export const AIDisplayFeedsPart = ({ part }: { part: AIDisplayFeedsTool }) => { return ( -
+
📡 diff --git a/apps/desktop/layer/renderer/src/modules/ai/chat/components/displays/AIDisplaySubscriptionsPart.tsx b/apps/desktop/layer/renderer/src/modules/ai/chat/components/displays/AIDisplaySubscriptionsPart.tsx index dbb8c3401..9b0653493 100644 --- a/apps/desktop/layer/renderer/src/modules/ai/chat/components/displays/AIDisplaySubscriptionsPart.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai/chat/components/displays/AIDisplaySubscriptionsPart.tsx @@ -254,7 +254,7 @@ export const AIDisplaySubscriptionsPart = ({ part }: { part: AIDisplaySubscripti return ( -
+
📋 diff --git a/apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/ChatInput.tsx b/apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/ChatInput.tsx index c28b5e56d..ee8a9b79c 100644 --- a/apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/ChatInput.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/ChatInput.tsx @@ -1,3 +1,5 @@ +import type { LexicalRichEditorRef } from "@follow/components/ui/lexical-rich-editor/index.js" +import { LexicalRichEditor } from "@follow/components/ui/lexical-rich-editor/index.js" import { cn, stopPropagation } from "@follow/utils" import type { VariantProps } from "class-variance-authority" import { cva } from "class-variance-authority" @@ -6,13 +8,10 @@ import { $getRoot } from "lexical" import { memo, useCallback, useRef, useState } from "react" import { AIChatContextBar } from "~/modules/ai/chat/components/AIChatContextBar" -import { convertLexicalToMarkdown } from "~/modules/ai/chat/utils/lexical-markdown" import { useChatActions, useChatError, useChatStatus } from "../../__internal__/hooks" import { AIChatSendButton } from "./AIChatSendButton" import { CollapsibleError } from "./CollapsibleError" -import type { LexicalRichEditorRef } from "./LexicalRichEditor" -import { LexicalRichEditor } from "./LexicalRichEditor" const chatInputVariants = cva( [ @@ -33,7 +32,7 @@ const chatInputVariants = cva( ) interface ChatInputProps extends VariantProps { - onSend: (message: string) => void + onSend: (message: EditorState | string, editor: LexicalEditor | null) => void } export const ChatInput = memo(({ onSend, variant }: ChatInputProps) => { @@ -52,11 +51,8 @@ export const ChatInput = memo(({ onSend, variant }: ChatInputProps) => { const handleSend = useCallback(() => { if (currentEditor && editorRef.current && !editorRef.current.isEmpty()) { - const markdown = convertLexicalToMarkdown(currentEditor) - if (markdown.trim()) { - onSend(markdown.trim()) - editorRef.current.clear() - } + onSend(currentEditor.getEditorState(), currentEditor) + editorRef.current.clear() } }, [currentEditor, onSend]) @@ -102,6 +98,7 @@ export const ChatInput = memo(({ onSend, variant }: ChatInputProps) => { onChange={handleEditorChange} onKeyDown={handleKeyDown} autoFocus + namespace="AIChatRichEditor" />
{
onSend(prompt, null)} />
diff --git a/apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/ChatInterface.tsx b/apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/ChatInterface.tsx index 25e13dbbc..0efd8b135 100644 --- a/apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/ChatInterface.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/ChatInterface.tsx @@ -1,6 +1,8 @@ import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js" import { cn, nextFrame } from "@follow/utils" import { springScrollTo } from "@follow/utils/scroller" +import type { BizUIMessage } from "@folo-services/ai-tools" +import type { EditorState, LexicalEditor } from "lexical" import { nanoid } from "nanoid" import { useCallback, useEffect, useRef, useState } from "react" import { useEventCallback } from "usehooks-ts" @@ -21,6 +23,7 @@ import { import { useAutoScroll } from "~/modules/ai/chat/hooks/useAutoScroll" import { useLoadMessages } from "~/modules/ai/chat/hooks/useLoadMessages" +import { convertLexicalToMarkdown } from "../../utils/lexical-markdown" import { ChatInput } from "./ChatInput" import { WelcomeScreen } from "./WelcomeScreen" @@ -88,11 +91,11 @@ export const ChatInterface = () => { }, []) const blockActions = useBlockActions() - const handleSendMessage = useEventCallback((message: string) => { - resetScrollState() + const handleSendMessage = useEventCallback( + (message: string | EditorState, editor: LexicalEditor | null) => { + resetScrollState() - chatActions.sendMessage({ - parts: [ + const parts: BizUIMessage["parts"] = [ { type: "data-block", data: blockActions.getBlocks().map((b) => ({ @@ -100,12 +103,30 @@ export const ChatInterface = () => { value: b.value, })), }, - { type: "text", text: message }, - ], - role: "user", - id: nanoid(), - }) - }) + ] + + if (typeof message === "string") { + parts.push({ + type: "text", + text: message, + }) + } else if (editor) { + parts.push({ + type: "data-rich-text", + data: { + state: message.toJSON(), + text: convertLexicalToMarkdown(editor), + }, + }) + } + + chatActions.sendMessage({ + parts, + role: "user", + id: nanoid(), + }) + }, + ) useEffect(() => { if (status === "submitted") { diff --git a/apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/WelcomeScreen.tsx b/apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/WelcomeScreen.tsx index 8c1b3e942..b28dc9071 100644 --- a/apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/WelcomeScreen.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/WelcomeScreen.tsx @@ -1,3 +1,4 @@ +import type { EditorState, LexicalEditor } from "lexical" import { m } from "motion/react" import { useTranslation } from "react-i18next" @@ -8,7 +9,7 @@ import { useSettingModal } from "~/modules/settings/modal/use-setting-modal-hack import { ChatInput } from "./ChatInput" interface WelcomeScreenProps { - onSend: (message: string) => void + onSend: (message: EditorState | string, editor: LexicalEditor | null) => void } const DEFAULT_SHORTCUTS = [ "Generate today daily report", @@ -71,7 +72,7 @@ export const WelcomeScreen = ({ onSend }: WelcomeScreenProps) => { initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: index * 0.1 }} - onClick={() => onSend(shortcut.prompt)} + onClick={() => onSend(shortcut.prompt, null)} title={shortcut.hotkey ? `${shortcut.name} (${shortcut.hotkey})` : shortcut.name} > {shortcut.name} @@ -87,7 +88,7 @@ export const WelcomeScreen = ({ onSend }: WelcomeScreenProps) => { initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: (enabledShortcuts.length + index) * 0.1 }} - onClick={() => onSend(suggestion)} + onClick={() => onSend(suggestion, null)} > {suggestion} diff --git a/apps/desktop/layer/renderer/src/modules/ai/chat/components/message/AIMessageParts.tsx b/apps/desktop/layer/renderer/src/modules/ai/chat/components/message/AIMessageParts.tsx index 50a8b9304..d17da01c9 100644 --- a/apps/desktop/layer/renderer/src/modules/ai/chat/components/message/AIMessageParts.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai/chat/components/message/AIMessageParts.tsx @@ -1,4 +1,5 @@ import type { ToolUIPart } from "ai" +import type { SerializedEditorState } from "lexical" import { m } from "motion/react" import * as React from "react" @@ -20,6 +21,7 @@ import { import { ToolInvocationComponent } from "../ToolInvocationComponent" import { AIDataBlockPart } from "./AIDataBlockPart" import { AIMarkdownMessage } from "./AIMarkdownMessage" +import { AIRichTextMessage } from "./AIRichTextMessage" interface MessagePartsProps { message: BizUIMessage @@ -95,6 +97,16 @@ export const AIMessageParts: React.FC = React.memo(({ message return } + case "data-rich-text": { + return ( + + ) + } + case "tool-displayAnalytics": { return } diff --git a/apps/desktop/layer/renderer/src/modules/ai/chat/components/message/AIRichTextMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai/chat/components/message/AIRichTextMessage.tsx new file mode 100644 index 000000000..cef7f88f4 --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/ai/chat/components/message/AIRichTextMessage.tsx @@ -0,0 +1,74 @@ +import { defaultLexicalTheme } from "@follow/components/ui/lexical-rich-editor/index.js" +import { cn } from "@follow/utils" +import { CodeHighlightNode, CodeNode } from "@lexical/code" +import { LinkNode } from "@lexical/link" +import { ListItemNode, ListNode } from "@lexical/list" +import { MarkNode } from "@lexical/mark" +import type { InitialConfigType } from "@lexical/react/LexicalComposer" +import { LexicalComposer } from "@lexical/react/LexicalComposer" +import { ContentEditable } from "@lexical/react/LexicalContentEditable" +import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary" +import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin" +import { HeadingNode, QuoteNode } from "@lexical/rich-text" +import type { SerializedEditorState } from "lexical" +import { ParagraphNode, TextNode } from "lexical" +import * as React from "react" + +function onError(error: Error) { + console.error("Lexical Read-Only Editor Error:", error) +} + +interface AIRichTextMessageProps { + data: { + state: SerializedEditorState + text: string + } + className?: string +} + +export const AIRichTextMessage: React.FC = ({ data, className }) => { + const initialConfig: InitialConfigType = { + namespace: "AIRichTextDisplay", + theme: defaultLexicalTheme, + onError, + editable: false, // Read-only mode + editorState: JSON.stringify(data.state), + nodes: [ + // Core nodes + ParagraphNode, + TextNode, + + // Rich text nodes + HeadingNode, + QuoteNode, + + // List nodes + ListNode, + ListItemNode, + + // Code nodes + CodeNode, + CodeHighlightNode, + + // Link nodes + LinkNode, + + // Text format nodes + MarkNode, + ], + } + + return ( +
+ + + } + ErrorBoundary={LexicalErrorBoundary} + placeholder={null} + /> + +
+ ) +} diff --git a/apps/desktop/layer/renderer/src/modules/ai/chat/services/index.ts b/apps/desktop/layer/renderer/src/modules/ai/chat/services/index.ts index f353566d1..81835a475 100644 --- a/apps/desktop/layer/renderer/src/modules/ai/chat/services/index.ts +++ b/apps/desktop/layer/renderer/src/modules/ai/chat/services/index.ts @@ -2,10 +2,8 @@ import type { AsyncDb } from "@follow/database/db" import { db } from "@follow/database/db" import { aiChatMessagesTable, aiChatTable } from "@follow/database/schemas/index" import { asc, count, eq, inArray, sql } from "drizzle-orm" -import type { SerializedEditorState } from "lexical" import type { BizUIMessage } from "../__internal__/types" -import type { MessageContent } from "../utils/lexical-markdown" class AIPersistServiceStatic { async loadMessages(chatId: string) { @@ -51,27 +49,6 @@ class AIPersistServiceStatic { return dbMessages.map((msg) => this.convertToUIMessage(msg)) } - /** - * Store a rich text message from user input - */ - async insertRichTextMessage(chatId: string, messageId: string, content: MessageContent) { - let richTextSchema: SerializedEditorState | undefined - - if (content.format === "richtext") { - richTextSchema = content.content as SerializedEditorState - } - - await db.insert(aiChatMessagesTable).values({ - id: messageId, - chatId, - role: "user", - - richTextSchema, - createdAt: new Date(), - status: "completed", - }) - } - async insertMessages(chatId: string, messages: BizUIMessage[]) { if (messages.length === 0) { return @@ -90,7 +67,6 @@ class AIPersistServiceStatic { role: message.role, contentFormat: "plaintext" as const, - richTextSchema: undefined, createdAt: new Date(), status: "completed" as const, finishedAt: message.metadata?.finishTime @@ -109,7 +85,6 @@ class AIPersistServiceStatic { finishedAt: sql`excluded.finished_at`, createdAt: sql`excluded.created_at`, status: sql`excluded.status`, - richTextSchema: sql`excluded.rich_text_schema`, }, }) } @@ -142,7 +117,6 @@ class AIPersistServiceStatic { chatId, role: message.role, contentFormat: "plaintext" as const, - richTextSchema: undefined, createdAt: new Date(), status: "completed" as const, finishedAt: message.metadata?.finishTime @@ -160,7 +134,6 @@ class AIPersistServiceStatic { metadata: sql`excluded.metadata`, finishedAt: sql`excluded.finished_at`, status: sql`excluded.status`, - richTextSchema: sql`excluded.rich_text_schema`, }, }) } @@ -239,7 +212,6 @@ class AIPersistServiceStatic { metadata: sql`excluded.metadata`, finishedAt: sql`excluded.finished_at`, status: sql`excluded.status`, - richTextSchema: sql`excluded.rich_text_schema`, }, }) } diff --git a/packages/internal/components/package.json b/packages/internal/components/package.json index a229a5802..7259d6edf 100644 --- a/packages/internal/components/package.json +++ b/packages/internal/components/package.json @@ -28,6 +28,13 @@ "@follow/types": "workspace:*", "@follow/utils": "workspace:*", "@headlessui/react": "2.2.4", + "@lexical/code": "0.33.1", + "@lexical/link": "0.33.1", + "@lexical/list": "0.33.1", + "@lexical/mark": "0.33.1", + "@lexical/markdown": "0.33.1", + "@lexical/react": "0.33.1", + "@lexical/rich-text": "0.33.1", "@microflash/remark-callout-directives": "4.4.0", "@radix-ui/react-accordion": "1.2.11", "@radix-ui/react-avatar": "1.1.10", @@ -60,6 +67,7 @@ "input-otp": "1.4.2", "jotai": "2.12.5", "katex": "0.16.22", + "lexical": "0.33.1", "masonic": "4.1.0", "motion": "12.23.0", "react-blurhash": "0.3.0", diff --git a/apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/LexicalRichEditor.tsx b/packages/internal/components/src/ui/lexical-rich-editor/LexicalRichEditor.tsx similarity index 54% rename from apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/LexicalRichEditor.tsx rename to packages/internal/components/src/ui/lexical-rich-editor/LexicalRichEditor.tsx index facfcff75..abfb88a79 100644 --- a/apps/desktop/layer/renderer/src/modules/ai/chat/components/layouts/LexicalRichEditor.tsx +++ b/packages/internal/components/src/ui/lexical-rich-editor/LexicalRichEditor.tsx @@ -6,7 +6,6 @@ import { MarkNode } from "@lexical/mark" import { TRANSFORMERS } from "@lexical/markdown" import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin" import { LexicalComposer } from "@lexical/react/LexicalComposer" -import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext" import { ContentEditable } from "@lexical/react/LexicalContentEditable" import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary" import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin" @@ -18,113 +17,16 @@ import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin" import { HeadingNode, QuoteNode } from "@lexical/rich-text" import type { EditorState, LexicalEditor } from "lexical" import { $getRoot, ParagraphNode, TextNode } from "lexical" -import { useEffect, useImperativeHandle, useRef, useState } from "react" +import { useImperativeHandle, useRef, useState } from "react" -const theme = { - paragraph: "mb-1", - text: { - bold: "font-semibold", - italic: "italic", - strikethrough: "line-through", - underline: "underline", - code: "bg-fill px-1 py-0.5 rounded text-sm font-mono", - }, - heading: { - h1: "text-2xl font-bold mb-2", - h2: "text-xl font-bold mb-2", - h3: "text-lg font-bold mb-1", - h4: "text-base font-bold mb-1", - h5: "text-sm font-bold mb-1", - h6: "text-xs font-bold mb-1", - }, - list: { - nested: { - listitem: "list-none", - }, - ol: "list-decimal list-inside mb-2", - ul: "list-disc list-inside mb-2", - listitem: "mb-1", - }, - quote: "border-l-4 border-accent pl-4 italic mb-2", - code: "bg-fill px-3 py-2 rounded font-mono text-sm mb-2 block overflow-x-auto", - codeHighlight: { - atrule: "text-purple-400", - attr: "text-blue-400", - boolean: "text-orange-400", - builtin: "text-purple-400", - cdata: "text-gray-400", - char: "text-green-400", - class: "text-blue-400", - "class-name": "text-blue-400", - comment: "text-gray-400", - constant: "text-orange-400", - deleted: "text-red-400", - doctype: "text-gray-400", - entity: "text-orange-400", - function: "text-yellow-400", - important: "text-red-400", - inserted: "text-green-400", - keyword: "text-purple-400", - namespace: "text-blue-400", - number: "text-orange-400", - operator: "text-pink-400", - prolog: "text-gray-400", - property: "text-blue-400", - punctuation: "text-gray-300", - regex: "text-green-400", - selector: "text-green-400", - string: "text-green-400", - symbol: "text-orange-400", - tag: "text-red-400", - url: "text-blue-400", - variable: "text-orange-400", - }, - link: "text-accent underline hover:text-accent/80", - mark: "bg-yellow-200 px-1 py-0.5 rounded", -} +import { KeyboardPlugin } from "./plugins" +import { defaultLexicalTheme } from "./theme" +import type { LexicalRichEditorProps, LexicalRichEditorRef } from "./types" function onError(error: Error) { console.error("Lexical Editor Error:", error) } -export interface LexicalRichEditorRef { - getEditor: () => LexicalEditor - focus: () => void - clear: () => void - isEmpty: () => boolean -} - -interface LexicalRichEditorProps { - placeholder?: string - className?: string - onChange?: (editorState: EditorState, editor: LexicalEditor) => void - onKeyDown?: (event: KeyboardEvent) => boolean - autoFocus?: boolean -} - -function KeyboardPlugin({ onKeyDown }: { onKeyDown?: (event: KeyboardEvent) => boolean }) { - const [editor] = useLexicalComposerContext() - - useEffect(() => { - if (!onKeyDown) return - - const handleKeyDown = (event: KeyboardEvent) => { - onKeyDown(event) - } - - return editor.registerRootListener((rootElement, prevRootElement) => { - if (prevRootElement !== null) { - prevRootElement.removeEventListener("keydown", handleKeyDown) - } - if (rootElement !== null) { - rootElement.addEventListener("keydown", handleKeyDown) - } - }) - }, [editor, onKeyDown]) - - return null -} - export const LexicalRichEditor = ({ ref, placeholder = "Enter your message...", @@ -132,12 +34,21 @@ export const LexicalRichEditor = ({ onChange, onKeyDown, autoFocus = false, + namespace = "LexicalRichEditor", + theme = defaultLexicalTheme, + enabledPlugins = { + history: true, + markdown: true, + list: true, + link: true, + autoFocus: true, + }, }: LexicalRichEditorProps & { ref?: React.RefObject }) => { const editorRef = useRef(null) const [isEmpty, setIsEmpty] = useState(true) const initialConfig = { - namespace: "AIChatRichEditor", + namespace, theme, onError, nodes: [ @@ -214,14 +125,17 @@ export const LexicalRichEditor = ({ ErrorBoundary={LexicalErrorBoundary} /> - - - - + + {enabledPlugins.history && } + {enabledPlugins.markdown && } + {enabledPlugins.list && } + {enabledPlugins.link && } - {autoFocus && } + {autoFocus && enabledPlugins.autoFocus && }
) } + +LexicalRichEditor.displayName = "LexicalRichEditor" diff --git a/packages/internal/components/src/ui/lexical-rich-editor/index.tsx b/packages/internal/components/src/ui/lexical-rich-editor/index.tsx new file mode 100644 index 000000000..5e8a0ca65 --- /dev/null +++ b/packages/internal/components/src/ui/lexical-rich-editor/index.tsx @@ -0,0 +1,4 @@ +export { LexicalRichEditor } from "./LexicalRichEditor" +export { KeyboardPlugin } from "./plugins" +export { defaultLexicalTheme } from "./theme" +export type { LexicalRichEditorProps, LexicalRichEditorRef } from "./types" diff --git a/packages/internal/components/src/ui/lexical-rich-editor/plugins/KeyboardPlugin.tsx b/packages/internal/components/src/ui/lexical-rich-editor/plugins/KeyboardPlugin.tsx new file mode 100644 index 000000000..58c07b4bc --- /dev/null +++ b/packages/internal/components/src/ui/lexical-rich-editor/plugins/KeyboardPlugin.tsx @@ -0,0 +1,29 @@ +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext" +import { useEffect } from "react" + +interface KeyboardPluginProps { + onKeyDown?: (event: KeyboardEvent) => boolean +} + +export function KeyboardPlugin({ onKeyDown }: KeyboardPluginProps) { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!onKeyDown) return + + const handleKeyDown = (event: KeyboardEvent) => { + onKeyDown(event) + } + + return editor.registerRootListener((rootElement, prevRootElement) => { + if (prevRootElement !== null) { + prevRootElement.removeEventListener("keydown", handleKeyDown) + } + if (rootElement !== null) { + rootElement.addEventListener("keydown", handleKeyDown) + } + }) + }, [editor, onKeyDown]) + + return null +} diff --git a/packages/internal/components/src/ui/lexical-rich-editor/plugins/index.ts b/packages/internal/components/src/ui/lexical-rich-editor/plugins/index.ts new file mode 100644 index 000000000..22d054444 --- /dev/null +++ b/packages/internal/components/src/ui/lexical-rich-editor/plugins/index.ts @@ -0,0 +1 @@ +export { KeyboardPlugin } from "./KeyboardPlugin" diff --git a/packages/internal/components/src/ui/lexical-rich-editor/theme.ts b/packages/internal/components/src/ui/lexical-rich-editor/theme.ts new file mode 100644 index 000000000..e77b0c75b --- /dev/null +++ b/packages/internal/components/src/ui/lexical-rich-editor/theme.ts @@ -0,0 +1,69 @@ +/** + * Default Lexical theme configuration for consistent styling + * across editable and read-only rich text components. + * + * Uses Follow's UIKit color system with Tailwind classes for + * automatic light/dark mode adaptation. + */ +export const defaultLexicalTheme = { + paragraph: "mb-1", + text: { + bold: "font-semibold", + italic: "italic", + strikethrough: "line-through", + underline: "underline", + code: "bg-fill px-1 py-0.5 rounded text-sm font-mono", + }, + heading: { + h1: "text-2xl font-bold mb-2", + h2: "text-xl font-bold mb-2", + h3: "text-lg font-bold mb-1", + h4: "text-base font-bold mb-1", + h5: "text-sm font-bold mb-1", + h6: "text-xs font-bold mb-1", + }, + list: { + nested: { + listitem: "list-none", + }, + ol: "list-decimal list-inside mb-2", + ul: "list-disc list-inside mb-2", + listitem: "mb-1", + }, + quote: "border-l-4 border-accent pl-4 italic mb-2", + code: "bg-fill px-3 py-2 rounded font-mono text-sm mb-2 block overflow-x-auto", + codeHighlight: { + atrule: "text-purple-400", + attr: "text-blue-400", + boolean: "text-orange-400", + builtin: "text-purple-400", + cdata: "text-gray-400", + char: "text-green-400", + class: "text-blue-400", + "class-name": "text-blue-400", + comment: "text-gray-400", + constant: "text-orange-400", + deleted: "text-red-400", + doctype: "text-gray-400", + entity: "text-orange-400", + function: "text-yellow-400", + important: "text-red-400", + inserted: "text-green-400", + keyword: "text-purple-400", + namespace: "text-blue-400", + number: "text-orange-400", + operator: "text-pink-400", + prolog: "text-gray-400", + property: "text-blue-400", + punctuation: "text-gray-300", + regex: "text-green-400", + selector: "text-green-400", + string: "text-green-400", + symbol: "text-orange-400", + tag: "text-red-400", + url: "text-blue-400", + variable: "text-orange-400", + }, + link: "text-accent underline hover:text-accent/80", + mark: "bg-yellow-200 px-1 py-0.5 rounded", +} diff --git a/packages/internal/components/src/ui/lexical-rich-editor/types.ts b/packages/internal/components/src/ui/lexical-rich-editor/types.ts new file mode 100644 index 000000000..4b866f344 --- /dev/null +++ b/packages/internal/components/src/ui/lexical-rich-editor/types.ts @@ -0,0 +1,25 @@ +import type { EditorState, LexicalEditor } from "lexical" + +export interface LexicalRichEditorRef { + getEditor: () => LexicalEditor + focus: () => void + clear: () => void + isEmpty: () => boolean +} + +export interface LexicalRichEditorProps { + placeholder?: string + className?: string + onChange?: (editorState: EditorState, editor: LexicalEditor) => void + onKeyDown?: (event: KeyboardEvent) => boolean + autoFocus?: boolean + namespace?: string + theme?: any + enabledPlugins?: { + history?: boolean + markdown?: boolean + list?: boolean + link?: boolean + autoFocus?: boolean + } +} diff --git a/packages/internal/database/src/drizzle/0034_curly_darkstar.sql b/packages/internal/database/src/drizzle/0034_curly_darkstar.sql new file mode 100644 index 000000000..b6be5b012 --- /dev/null +++ b/packages/internal/database/src/drizzle/0034_curly_darkstar.sql @@ -0,0 +1 @@ +ALTER TABLE `ai_chat_messages` DROP COLUMN `rich_text_schema`; \ No newline at end of file diff --git a/packages/internal/database/src/drizzle/meta/0034_snapshot.json b/packages/internal/database/src/drizzle/meta/0034_snapshot.json new file mode 100644 index 000000000..c68afacc1 --- /dev/null +++ b/packages/internal/database/src/drizzle/meta/0034_snapshot.json @@ -0,0 +1,938 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6cfe24ba-739c-4f3f-a894-dd70ecb47d02", + "prevId": "339611d0-cf1e-4ec9-9bf6-275608b815f5", + "tables": { + "ai_chat_messages": { + "name": "ai_chat_messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'completed'" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message_parts": { + "name": "message_parts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_ai_chat_messages_chat_id_created_at": { + "name": "idx_ai_chat_messages_chat_id_created_at", + "columns": ["chat_id", "created_at"], + "isUnique": false + }, + "idx_ai_chat_messages_status": { + "name": "idx_ai_chat_messages_status", + "columns": ["status"], + "isUnique": false + }, + "idx_ai_chat_messages_chat_id_role": { + "name": "idx_ai_chat_messages_chat_id_role", + "columns": ["chat_id", "role"], + "isUnique": false + } + }, + "foreignKeys": { + "ai_chat_messages_chat_id_ai_chat_sessions_id_fk": { + "name": "ai_chat_messages_chat_id_ai_chat_sessions_id_fk", + "tableFrom": "ai_chat_messages", + "tableTo": "ai_chat_sessions", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_chat_sessions": { + "name": "ai_chat_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "idx_ai_chat_sessions_updated_at": { + "name": "idx_ai_chat_sessions_updated_at", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "collections": { + "name": "collections", + "columns": { + "feed_id": { + "name": "feed_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entry_id": { + "name": "entry_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "entries": { + "name": "entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_content": { + "name": "source_content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "readability_updated_at": { + "name": "readability_updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guid": { + "name": "guid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_url": { + "name": "author_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_avatar": { + "name": "author_avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inserted_at": { + "name": "inserted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "media": { + "name": "media", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "categories": { + "name": "categories", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachments": { + "name": "attachments", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extra": { + "name": "extra", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feed_id": { + "name": "feed_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inbox_handle": { + "name": "inbox_handle", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read": { + "name": "read", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "settings": { + "name": "settings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feeds": { + "name": "feeds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_at": { + "name": "error_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "site_url": { + "name": "site_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_count": { + "name": "subscription_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updates_per_week": { + "name": "updates_per_week", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_entry_published_at": { + "name": "latest_entry_published_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tip_users": { + "name": "tip_users", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "images": { + "name": "images", + "columns": { + "url": { + "name": "url", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "colors": { + "name": "colors", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inboxes": { + "name": "inboxes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lists": { + "name": "lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "feed_ids": { + "name": "feed_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fee": { + "name": "fee", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_count": { + "name": "subscription_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purchase_amount": { + "name": "purchase_amount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "subscriptions": { + "name": "subscriptions", + "columns": { + "feed_id": { + "name": "feed_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "list_id": { + "name": "list_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inbox_id": { + "name": "inbox_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_private": { + "name": "is_private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hide_from_timeline": { + "name": "hide_from_timeline", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "summaries": { + "name": "summaries", + "columns": { + "entry_id": { + "name": "entry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "readability_summary": { + "name": "readability_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "unq": { + "name": "unq", + "columns": ["entry_id", "language"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "translations": { + "name": "translations", + "columns": { + "entry_id": { + "name": "entry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "readability_content": { + "name": "readability_content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "translation-unique-index": { + "name": "translation-unique-index", + "columns": ["entry_id", "language"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "unread": { + "name": "unread", + "columns": { + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_me": { + "name": "is_me", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "social_links": { + "name": "social_links", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/packages/internal/database/src/drizzle/meta/_journal.json b/packages/internal/database/src/drizzle/meta/_journal.json index ebea5d4fc..9d90c0f87 100644 --- a/packages/internal/database/src/drizzle/meta/_journal.json +++ b/packages/internal/database/src/drizzle/meta/_journal.json @@ -239,6 +239,13 @@ "when": 1753890574250, "tag": "0033_shiny_sebastian_shaw", "breakpoints": true + }, + { + "idx": 34, + "version": "6", + "when": 1753937164043, + "tag": "0034_curly_darkstar", + "breakpoints": true } ] } diff --git a/packages/internal/database/src/drizzle/migrations.js b/packages/internal/database/src/drizzle/migrations.js index 6cfccdcf3..0ec674a8d 100644 --- a/packages/internal/database/src/drizzle/migrations.js +++ b/packages/internal/database/src/drizzle/migrations.js @@ -34,6 +34,7 @@ import m0030 from "./0030_common_gabe_jones.sql" import m0031 from "./0031_kind_ikaris.sql" import m0032 from "./0032_orange_prima.sql" import m0033 from "./0033_shiny_sebastian_shaw.sql" +import m0034 from "./0034_curly_darkstar.sql" import journal from "./meta/_journal.json" export default { @@ -73,5 +74,6 @@ export default { m0031, m0032, m0033, + m0034, }, } diff --git a/packages/internal/database/src/schemas/index.ts b/packages/internal/database/src/schemas/index.ts index dca78555b..43411949a 100644 --- a/packages/internal/database/src/schemas/index.ts +++ b/packages/internal/database/src/schemas/index.ts @@ -228,18 +228,11 @@ export const aiChatMessagesTable = sqliteTable( .notNull() .references(() => aiChatTable.chatId, { onDelete: "cascade" }), - // Core message properties matching Vercel AI SDK UIMessage role: t.text("role").notNull().$type<"user" | "assistant" | "system">(), - richTextSchema: t - .text("rich_text_schema", { mode: "json" }) - .$type(), // Lexical schema for user rich text - - // Vercel AI SDK UIMessage properties createdAt: t.integer("created_at", { mode: "timestamp_ms" }), metadata: t.text("metadata", { mode: "json" }).$type(), - // Message processing status status: t .text("status") .$type<"pending" | "streaming" | "completed" | "error">() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df39534c9..4e9b40056 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1462,6 +1462,27 @@ importers: '@headlessui/react': specifier: 2.2.4 version: 2.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@lexical/code': + specifier: 0.33.1 + version: 0.33.1 + '@lexical/link': + specifier: 0.33.1 + version: 0.33.1 + '@lexical/list': + specifier: 0.33.1 + version: 0.33.1 + '@lexical/mark': + specifier: 0.33.1 + version: 0.33.1 + '@lexical/markdown': + specifier: 0.33.1 + version: 0.33.1 + '@lexical/react': + specifier: 0.33.1 + version: 0.33.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(yjs@13.6.27) + '@lexical/rich-text': + specifier: 0.33.1 + version: 0.33.1 '@microflash/remark-callout-directives': specifier: 4.4.0 version: 4.4.0(patch_hash=6160b1cf0eab6deca36415693dda703746edcd42e0e64b9174294e2210481396) @@ -1558,6 +1579,9 @@ importers: katex: specifier: 0.16.22 version: 0.16.22 + lexical: + specifier: 0.33.1 + version: 0.33.1 masonic: specifier: 4.1.0 version: 4.1.0(react@19.0.0)