From 4bc961ffb4fbf8264f02e63d3e1ffc5a7932a526 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Wed, 13 Aug 2025 17:25:21 -0700 Subject: [PATCH] Fix preset value (#933) --- src/components/chat/ChatInput.tsx | 1 + src/components/chat/HomeChatInput.tsx | 1 + src/components/chat/LexicalChatInput.tsx | 81 ++++++++++++++++++++++-- src/shared/parse_mention_apps.ts | 6 +- 4 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx index 8e45513..7dafef8 100644 --- a/src/components/chat/ChatInput.tsx +++ b/src/components/chat/ChatInput.tsx @@ -293,6 +293,7 @@ export function ChatInput({ chatId }: { chatId?: number }) { onSubmit={handleSubmit} onPaste={handlePaste} placeholder="Ask Dyad to build..." + excludeCurrentApp={true} /> {isStreaming ? ( diff --git a/src/components/chat/HomeChatInput.tsx b/src/components/chat/HomeChatInput.tsx index eb64087..1f39873 100644 --- a/src/components/chat/HomeChatInput.tsx +++ b/src/components/chat/HomeChatInput.tsx @@ -83,6 +83,7 @@ export function HomeChatInput({ onPaste={handlePaste} placeholder="Ask Dyad to build..." disabled={isStreaming} + excludeCurrentApp={false} /> {/* File attachment dropdown */} diff --git a/src/components/chat/LexicalChatInput.tsx b/src/components/chat/LexicalChatInput.tsx index 2a74552..6d42c50 100644 --- a/src/components/chat/LexicalChatInput.tsx +++ b/src/components/chat/LexicalChatInput.tsx @@ -1,5 +1,10 @@ import React, { useCallback, useEffect, useState } from "react"; -import { $getRoot, $createParagraphNode, EditorState } from "lexical"; +import { + $getRoot, + $createParagraphNode, + $createTextNode, + EditorState, +} from "lexical"; import { LexicalComposer } from "@lexical/react/LexicalComposer"; import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin"; import { ContentEditable } from "@lexical/react/LexicalContentEditable"; @@ -10,6 +15,7 @@ import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"; import { BeautifulMentionsPlugin, BeautifulMentionNode, + $createBeautifulMentionNode, type BeautifulMentionsTheme, type BeautifulMentionsMenuItemProps, } from "lexical-beautiful-mentions"; @@ -18,7 +24,7 @@ import { useLoadApps } from "@/hooks/useLoadApps"; import { forwardRef } from "react"; import { useAtomValue } from "jotai"; import { selectedAppIdAtom } from "@/atoms/appAtoms"; -import { parseAppMentions } from "@/shared/parse_mention_apps"; +import { MENTION_REGEX, parseAppMentions } from "@/shared/parse_mention_apps"; // Define the theme for mentions const beautifulMentionsTheme: BeautifulMentionsTheme = { @@ -129,6 +135,68 @@ function ClearEditorPlugin({ return null; } +// Plugin to sync external value prop into the editor +function ExternalValueSyncPlugin({ value }: { value: string }) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + // Derive the display text that should appear in the editor (@Name) from the + // internal value representation (@app:Name) + const displayText = (value || "").replace(MENTION_REGEX, "@$1"); + + const currentText = editor.getEditorState().read(() => { + const root = $getRoot(); + return root.getTextContent(); + }); + + // If the editor already reflects the same display text, do nothing to avoid loops + if (currentText === displayText) return; + editor.update(() => { + const root = $getRoot(); + root.clear(); + + const paragraph = $createParagraphNode(); + + // Build nodes from the internal value, turning @app:Name into a mention node + const mentionRegex = /@app:([a-zA-Z0-9_-]+)/g; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = mentionRegex.exec(value)) !== null) { + const [full, name] = match; + const start = match.index; + + // Append any text before the mention + if (start > lastIndex) { + const textBefore = value.slice(lastIndex, start); + if (textBefore) paragraph.append($createTextNode(textBefore)); + } + + // Append the actual mention node (@ trigger with value = Name) + paragraph.append($createBeautifulMentionNode("@", name)); + + lastIndex = start + full.length; + } + + // Append any trailing text after the last mention + if (lastIndex < value.length) { + const trailing = value.slice(lastIndex); + if (trailing) paragraph.append($createTextNode(trailing)); + } + + // If there were no mentions at all, just append the raw value as text + if (value && paragraph.getTextContent() === "") { + paragraph.append($createTextNode(value)); + } + + root.append(paragraph); + paragraph.selectEnd(); + }); + }, [editor, value]); + + return null; +} + interface LexicalChatInputProps { value: string; onChange: (value: string) => void; @@ -136,6 +204,7 @@ interface LexicalChatInputProps { onPaste?: (e: React.ClipboardEvent) => void; placeholder?: string; disabled?: boolean; + excludeCurrentApp: boolean; } function onError(error: Error) { @@ -147,6 +216,7 @@ export function LexicalChatInput({ onChange, onSubmit, onPaste, + excludeCurrentApp, placeholder = "Ask Dyad to build...", disabled = false, }: LexicalChatInputProps) { @@ -168,7 +238,7 @@ export function LexicalChatInput({ // Filter out current app and already mentioned apps const filteredApps = apps.filter((app) => { // Exclude current app - if (app.name === currentAppName) return false; + if (excludeCurrentApp && app.name === currentAppName) return false; // Exclude already mentioned apps (case-insensitive comparison) if ( @@ -185,7 +255,7 @@ export function LexicalChatInput({ return { "@": appMentions, }; - }, [apps, selectedAppId, value]); + }, [apps, selectedAppId, value, excludeCurrentApp]); const initialConfig = { namespace: "ChatInput", @@ -203,7 +273,6 @@ export function LexicalChatInput({ const root = $getRoot(); let textContent = root.getTextContent(); - console.time("handleEditorChange"); // Transform @AppName mentions to @app:AppName format // This regex matches @AppName where AppName is one of our actual app names @@ -223,7 +292,6 @@ export function LexicalChatInput({ textContent = textContent.replace(mentionRegex, "@app:$1"); } } - console.timeEnd("handleEditorChange"); onChange(textContent); }); }, @@ -275,6 +343,7 @@ export function LexicalChatInput({ +