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 <tukon479@gmail.com>
This commit is contained in:
parent
eb7e6515e1
commit
73a1e6eb6c
|
|
@ -343,7 +343,7 @@ export const AIDisplayEntriesPart = ({ part }: { part: AIDisplayEntriesTool }) =
|
|||
|
||||
return (
|
||||
<Card className="mb-2 w-full min-w-0">
|
||||
<div className="w-[9999px]" />
|
||||
<div className="w-[9999px] max-w-[calc(var(--ai-chat-layout-width,65ch)_-120px)]" />
|
||||
<CardHeader>
|
||||
<CardTitle className="text-text flex items-center gap-2 text-xl font-semibold">
|
||||
<span className="text-lg">📰</span>
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ export const AIDisplayFeedsPart = ({ part }: { part: AIDisplayFeedsTool }) => {
|
|||
|
||||
return (
|
||||
<Card className="mb-2 w-full min-w-0">
|
||||
<div className="w-[9999px]" />
|
||||
<div className="w-[9999px] max-w-[calc(var(--ai-chat-layout-width,65ch)_-120px)]" />
|
||||
<CardHeader>
|
||||
<CardTitle className="text-text flex items-center gap-2 text-xl font-semibold">
|
||||
<span className="text-lg">📡</span>
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ export const AIDisplaySubscriptionsPart = ({ part }: { part: AIDisplaySubscripti
|
|||
|
||||
return (
|
||||
<Card className="mb-2 w-full min-w-0">
|
||||
<div className="w-[9999px]" />
|
||||
<div className="w-[9999px] max-w-[calc(var(--ai-chat-layout-width,65ch)_-120px)]" />
|
||||
<CardHeader>
|
||||
<CardTitle className="text-text flex items-center gap-2 text-xl font-semibold">
|
||||
<span className="text-lg">📋</span>
|
||||
|
|
|
|||
|
|
@ -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<typeof chatInputVariants> {
|
||||
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"
|
||||
/>
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
<AIChatSendButton
|
||||
|
|
@ -117,7 +114,7 @@ export const ChatInput = memo(({ onSend, variant }: ChatInputProps) => {
|
|||
<div className="border-border/20 relative z-10 border-t bg-transparent">
|
||||
<AIChatContextBar
|
||||
className="border-0 bg-transparent px-4 py-2.5"
|
||||
onSendShortcut={onSend}
|
||||
onSendShortcut={(prompt) => onSend(prompt, null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
</m.button>
|
||||
|
|
|
|||
|
|
@ -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<MessagePartsProps> = React.memo(({ message
|
|||
return <AIDataBlockPart key={partKey} blocks={part.data as AIChatContextBlock[]} />
|
||||
}
|
||||
|
||||
case "data-rich-text": {
|
||||
return (
|
||||
<AIRichTextMessage
|
||||
key={partKey}
|
||||
data={part.data as { state: SerializedEditorState; text: string }}
|
||||
className={isUser ? "text-white" : "text-text"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
case "tool-displayAnalytics": {
|
||||
return <AIDisplayAnalyticsPart key={partKey} part={part as AIDisplayAnalyticsTool} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AIRichTextMessageProps> = ({ 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 (
|
||||
<div className={cn("text-text relative text-sm", className)}>
|
||||
<LexicalComposer initialConfig={initialConfig}>
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<ContentEditable className="focus:outline-none" style={{ outline: "none" }} />
|
||||
}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
placeholder={null}
|
||||
/>
|
||||
</LexicalComposer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<LexicalRichEditorRef | null> }) => {
|
||||
const editorRef = useRef<LexicalEditor | null>(null)
|
||||
const [isEmpty, setIsEmpty] = useState(true)
|
||||
|
||||
const initialConfig = {
|
||||
namespace: "AIChatRichEditor",
|
||||
namespace,
|
||||
theme,
|
||||
onError,
|
||||
nodes: [
|
||||
|
|
@ -214,14 +125,17 @@ export const LexicalRichEditor = ({
|
|||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<OnChangePlugin onChange={handleChange} />
|
||||
<HistoryPlugin />
|
||||
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
|
||||
<ListPlugin />
|
||||
<LinkPlugin />
|
||||
|
||||
{enabledPlugins.history && <HistoryPlugin />}
|
||||
{enabledPlugins.markdown && <MarkdownShortcutPlugin transformers={TRANSFORMERS} />}
|
||||
{enabledPlugins.list && <ListPlugin />}
|
||||
{enabledPlugins.link && <LinkPlugin />}
|
||||
|
||||
<KeyboardPlugin onKeyDown={onKeyDown} />
|
||||
{autoFocus && <AutoFocusPlugin />}
|
||||
{autoFocus && enabledPlugins.autoFocus && <AutoFocusPlugin />}
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
)
|
||||
}
|
||||
|
||||
LexicalRichEditor.displayName = "LexicalRichEditor"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export { LexicalRichEditor } from "./LexicalRichEditor"
|
||||
export { KeyboardPlugin } from "./plugins"
|
||||
export { defaultLexicalTheme } from "./theme"
|
||||
export type { LexicalRichEditorProps, LexicalRichEditorRef } from "./types"
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { KeyboardPlugin } from "./KeyboardPlugin"
|
||||
|
|
@ -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",
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE `ai_chat_messages` DROP COLUMN `rich_text_schema`;
|
||||
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<import("lexical").SerializedEditorState>(), // 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<any>(),
|
||||
|
||||
// Message processing status
|
||||
status: t
|
||||
.text("status")
|
||||
.$type<"pending" | "streaming" | "completed" | "error">()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue