diff --git a/web/src/app/_components/markdown.tsx b/web/src/app/_components/markdown.tsx index f470108..e36319b 100644 --- a/web/src/app/_components/markdown.tsx +++ b/web/src/app/_components/markdown.tsx @@ -39,7 +39,10 @@ export function Markdown({ }, [animate]); return (
-

- - {plan.title !== undefined && plan.title !== "" + + {`### ${ + plan.title !== undefined && plan.title !== "" ? plan.title - : "Deep Research"} - -

+ : "Deep Research" + }`} +
diff --git a/web/src/app/_components/research-report-block.tsx b/web/src/app/_components/research-report-block.tsx index 11275f4..ac8f121 100644 --- a/web/src/app/_components/research-report-block.tsx +++ b/web/src/app/_components/research-report-block.tsx @@ -1,10 +1,10 @@ // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates // SPDX-License-Identifier: MIT -import { useRef } from "react"; +import { useCallback, useEffect, useLayoutEffect, useRef } from "react"; import ReportEditor from "~/components/editor"; -import { useMessage } from "~/core/store"; +import { useMessage, useStore } from "~/core/store"; import { cn } from "~/lib/utils"; import { LoadingAnimation } from "./loading-animation"; @@ -19,15 +19,45 @@ export function ResearchReportBlock({ messageId: string; }) { const message = useMessage(messageId); + const handleMarkdownChange = useCallback( + (markdown: string) => { + if (message) { + message.content = markdown; + useStore.setState({ + messages: new Map(useStore.getState().messages).set( + message.id, + message, + ), + }); + } + }, + [message], + ); const contentRef = useRef(null); const isCompleted = message?.isStreaming === false && message?.content !== ""; + // TODO: scroll to top when completed, but it's not working + // useEffect(() => { + // if (isCompleted && contentRef.current) { + // setTimeout(() => { + // contentRef + // .current!.closest("[data-radix-scroll-area-viewport]") + // ?.scrollTo({ + // top: 0, + // behavior: "smooth", + // }); + // }, 500); + // } + // }, [isCompleted]); return (
{isCompleted ? ( - + ) : ( <> {message?.content} diff --git a/web/src/components/editor/generative/ai-selector.tsx b/web/src/components/editor/generative/ai-selector.tsx index 0c676bf..2eeac89 100644 --- a/web/src/components/editor/generative/ai-selector.tsx +++ b/web/src/components/editor/generative/ai-selector.tsx @@ -99,7 +99,7 @@ export function AISelector({ onOpenChange }: AISelectorProps) { {hasCompletion && (
-
+
{completion}
diff --git a/web/src/components/editor/index.tsx b/web/src/components/editor/index.tsx index ac26c6f..1896dab 100644 --- a/web/src/components/editor/index.tsx +++ b/web/src/components/editor/index.tsx @@ -38,12 +38,12 @@ const extensions = [...defaultExtensions, slashCommand]; export interface ReportEditorProps { content: Content; + onMarkdownChange?: (markdown: string) => void; } -const ReportEditor = ({ content }: ReportEditorProps) => { +const ReportEditor = ({ content, onMarkdownChange }: ReportEditorProps) => { const [initialContent, setInitialContent] = useState(() => content); const [saveStatus, setSaveStatus] = useState("Saved"); - const [charsCount, setCharsCount] = useState(); const [openNode, setOpenNode] = useState(false); const [openColor, setOpenColor] = useState(false); @@ -63,17 +63,21 @@ const ReportEditor = ({ content }: ReportEditorProps) => { const debouncedUpdates = useDebouncedCallback( async (editor: EditorInstance) => { - const json = editor.getJSON(); - setCharsCount(editor.storage.characterCount.words()); - window.localStorage.setItem( - "html-content", - highlightCodeblocks(editor.getHTML()), - ); - window.localStorage.setItem("novel-content", JSON.stringify(json)); - window.localStorage.setItem( - "markdown", - editor.storage.markdown.getMarkdown(), - ); + // const json = editor.getJSON(); + // // setCharsCount(editor.storage.characterCount.words()); + // window.localStorage.setItem( + // "html-content", + // highlightCodeblocks(editor.getHTML()), + // ); + // window.localStorage.setItem("novel-content", JSON.stringify(json)); + // window.localStorage.setItem( + // "markdown", + // editor.storage.markdown.getMarkdown(), + // ); + if (onMarkdownChange) { + const markdown = editor.storage.markdown.getMarkdown(); + onMarkdownChange(markdown); + } setSaveStatus("Saved"); }, 500, @@ -89,26 +93,12 @@ const ReportEditor = ({ content }: ReportEditorProps) => { return (
-
-
- {saveStatus} -
-
- {charsCount} Words -
-
handleCommandNavigation(event), diff --git a/web/src/components/editor/slash-command.tsx b/web/src/components/editor/slash-command.tsx index 4a69435..f24c5ff 100644 --- a/web/src/components/editor/slash-command.tsx +++ b/web/src/components/editor/slash-command.tsx @@ -1,32 +1,18 @@ import { - Brain, CheckSquare, Code, Heading1, Heading2, Heading3, - ImageIcon, List, ListOrdered, - MessageSquarePlus, Text, TextQuote, - Twitter, - Youtube, } from "lucide-react"; import { Command, createSuggestionItems, renderItems } from "novel"; -import { uploadFn } from "./image-upload"; +// import { uploadFn } from "./image-upload"; export const suggestionItems = createSuggestionItems([ - { - title: "Send Feedback", - description: "Let us know how we can improve.", - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).run(); - window.open("/feedback", "_blank"); - }, - }, { title: "Text", description: "Just start typing with plain text.", @@ -132,83 +118,83 @@ export const suggestionItems = createSuggestionItems([ command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), }, - { - title: "Image", - description: "Upload an image from your computer.", - searchTerms: ["photo", "picture", "media"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).run(); - // upload image - const input = document.createElement("input"); - input.type = "file"; - input.accept = "image/*"; - input.onchange = async () => { - if (input.files?.length) { - const file = input.files[0]; - if (!file) return; - const pos = editor.view.state.selection.from; - uploadFn(file, editor.view, pos); - } - }; - input.click(); - }, - }, - { - title: "Youtube", - description: "Embed a Youtube video.", - searchTerms: ["video", "youtube", "embed"], - icon: , - command: ({ editor, range }) => { - const videoLink = prompt("Please enter Youtube Video Link"); - //From https://regexr.com/3dj5t - const ytRegex = new RegExp( - /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/, - ); + // { + // title: "Image", + // description: "Upload an image from your computer.", + // searchTerms: ["photo", "picture", "media"], + // icon: , + // command: ({ editor, range }) => { + // editor.chain().focus().deleteRange(range).run(); + // // upload image + // const input = document.createElement("input"); + // input.type = "file"; + // input.accept = "image/*"; + // input.onchange = async () => { + // if (input.files?.length) { + // const file = input.files[0]; + // if (!file) return; + // const pos = editor.view.state.selection.from; + // uploadFn(file, editor.view, pos); + // } + // }; + // input.click(); + // }, + // }, + // { + // title: "Youtube", + // description: "Embed a Youtube video.", + // searchTerms: ["video", "youtube", "embed"], + // icon: , + // command: ({ editor, range }) => { + // const videoLink = prompt("Please enter Youtube Video Link"); + // //From https://regexr.com/3dj5t + // const ytRegex = new RegExp( + // /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/, + // ); - if (videoLink && ytRegex.test(videoLink)) { - editor - .chain() - .focus() - .deleteRange(range) - .setYoutubeVideo({ - src: videoLink, - }) - .run(); - } else { - if (videoLink !== null) { - alert("Please enter a correct Youtube Video Link"); - } - } - }, - }, - { - title: "Twitter", - description: "Embed a Tweet.", - searchTerms: ["twitter", "embed"], - icon: , - command: ({ editor, range }) => { - const tweetLink = prompt("Please enter Twitter Link"); - const tweetRegex = new RegExp( - /^https?:\/\/(www\.)?x\.com\/([a-zA-Z0-9_]{1,15})(\/status\/(\d+))?(\/\S*)?$/, - ); + // if (videoLink && ytRegex.test(videoLink)) { + // editor + // .chain() + // .focus() + // .deleteRange(range) + // .setYoutubeVideo({ + // src: videoLink, + // }) + // .run(); + // } else { + // if (videoLink !== null) { + // alert("Please enter a correct Youtube Video Link"); + // } + // } + // }, + // }, + // { + // title: "Twitter", + // description: "Embed a Tweet.", + // searchTerms: ["twitter", "embed"], + // icon: , + // command: ({ editor, range }) => { + // const tweetLink = prompt("Please enter Twitter Link"); + // const tweetRegex = new RegExp( + // /^https?:\/\/(www\.)?x\.com\/([a-zA-Z0-9_]{1,15})(\/status\/(\d+))?(\/\S*)?$/, + // ); - if (tweetLink && tweetRegex.test(tweetLink)) { - editor - .chain() - .focus() - .deleteRange(range) - .setTweet({ - src: tweetLink, - }) - .run(); - } else { - if (tweetLink !== null) { - alert("Please enter a correct Twitter Link"); - } - } - }, - }, + // if (tweetLink && tweetRegex.test(tweetLink)) { + // editor + // .chain() + // .focus() + // .deleteRange(range) + // .setTweet({ + // src: tweetLink, + // }) + // .run(); + // } else { + // if (tweetLink !== null) { + // alert("Please enter a correct Twitter Link"); + // } + // } + // }, + // }, ]); export const slashCommand = Command.configure({ diff --git a/web/src/core/api/chat.ts b/web/src/core/api/chat.ts index 6e04483..3f25edc 100644 --- a/web/src/core/api/chat.ts +++ b/web/src/core/api/chat.ts @@ -30,7 +30,7 @@ export async function* chatStream( options: { abortSignal?: AbortSignal } = {}, ) { if (location.search.includes("mock") || location.search.includes("replay=")) { - return chatReplayStream(userMessage, params, options); + return yield* chatReplayStream(userMessage, params, options); } const stream = fetchStream(resolveServiceURL("chat/stream"), { body: JSON.stringify({ diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css index c4b0047..8390da8 100644 --- a/web/src/styles/globals.css +++ b/web/src/styles/globals.css @@ -201,94 +201,6 @@ textarea { outline: none; } -.markdown { - line-height: 1.75; - - a { - text-decoration: underline; - text-decoration-style: dotted; - text-underline-position: from-font; - text-decoration-color: var(--muted-foreground); - - &:hover { - text-decoration: underline; - } - } - - h1 { - @apply text-2xl font-bold; - margin-top: 0.5rem; - margin-bottom: 0.5rem; - } - - h2 { - @apply text-xl font-bold; - margin-top: 0.5rem; - margin-bottom: 0.5rem; - } - - h3 { - @apply text-lg font-bold; - margin-top: 0.5rem; - margin-bottom: 0.5rem; - } - - h4 { - @apply text-base font-bold; - margin-bottom: 0.5rem; - } - - h5 { - @apply text-sm font-bold; - } - - h6 { - @apply text-xs font-bold; - } - - ul { - @apply list-disc pl-4; - } - - ol { - @apply list-decimal pl-4; - } - - img { - display: block; - max-width: 100%; - margin: 0 auto; - } - - table { - @apply w-full; - table-layout: fixed; - border-collapse: collapse; - - th, - td { - padding: 4px 8px; - border: 1px solid var(--border); - } - - th { - @apply bg-muted; - } - } - - code:not([class*="language-"]) { - @apply bg-muted; - @apply rounded-md; - @apply px-1; - @apply py-0.5; - @apply border; - } - - blockquote { - @apply border-muted-foreground text-muted-foreground border-l-2 pl-2; - } -} - @layer base { * { @apply border-border outline-ring/50; diff --git a/web/src/styles/prosemirror.css b/web/src/styles/prosemirror.css index 1f3b40e..53ed927 100644 --- a/web/src/styles/prosemirror.css +++ b/web/src/styles/prosemirror.css @@ -1,7 +1,6 @@ @import "./globals.css"; .ProseMirror { - @apply p-12 px-8 sm:px-12; } .ProseMirror .is-editor-empty:first-child::before {