Fix preset value (#933)

This commit is contained in:
Will Chen
2025-08-13 17:25:21 -07:00
committed by GitHub
parent b8362a74a7
commit 4bc961ffb4
4 changed files with 81 additions and 8 deletions

View File

@@ -293,6 +293,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
onSubmit={handleSubmit} onSubmit={handleSubmit}
onPaste={handlePaste} onPaste={handlePaste}
placeholder="Ask Dyad to build..." placeholder="Ask Dyad to build..."
excludeCurrentApp={true}
/> />
{isStreaming ? ( {isStreaming ? (

View File

@@ -83,6 +83,7 @@ export function HomeChatInput({
onPaste={handlePaste} onPaste={handlePaste}
placeholder="Ask Dyad to build..." placeholder="Ask Dyad to build..."
disabled={isStreaming} disabled={isStreaming}
excludeCurrentApp={false}
/> />
{/* File attachment dropdown */} {/* File attachment dropdown */}

View File

@@ -1,5 +1,10 @@
import React, { useCallback, useEffect, useState } from "react"; 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 { LexicalComposer } from "@lexical/react/LexicalComposer";
import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin"; import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable"; import { ContentEditable } from "@lexical/react/LexicalContentEditable";
@@ -10,6 +15,7 @@ import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { import {
BeautifulMentionsPlugin, BeautifulMentionsPlugin,
BeautifulMentionNode, BeautifulMentionNode,
$createBeautifulMentionNode,
type BeautifulMentionsTheme, type BeautifulMentionsTheme,
type BeautifulMentionsMenuItemProps, type BeautifulMentionsMenuItemProps,
} from "lexical-beautiful-mentions"; } from "lexical-beautiful-mentions";
@@ -18,7 +24,7 @@ import { useLoadApps } from "@/hooks/useLoadApps";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms"; 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 // Define the theme for mentions
const beautifulMentionsTheme: BeautifulMentionsTheme = { const beautifulMentionsTheme: BeautifulMentionsTheme = {
@@ -129,6 +135,68 @@ function ClearEditorPlugin({
return null; 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 { interface LexicalChatInputProps {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
@@ -136,6 +204,7 @@ interface LexicalChatInputProps {
onPaste?: (e: React.ClipboardEvent) => void; onPaste?: (e: React.ClipboardEvent) => void;
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
excludeCurrentApp: boolean;
} }
function onError(error: Error) { function onError(error: Error) {
@@ -147,6 +216,7 @@ export function LexicalChatInput({
onChange, onChange,
onSubmit, onSubmit,
onPaste, onPaste,
excludeCurrentApp,
placeholder = "Ask Dyad to build...", placeholder = "Ask Dyad to build...",
disabled = false, disabled = false,
}: LexicalChatInputProps) { }: LexicalChatInputProps) {
@@ -168,7 +238,7 @@ export function LexicalChatInput({
// Filter out current app and already mentioned apps // Filter out current app and already mentioned apps
const filteredApps = apps.filter((app) => { const filteredApps = apps.filter((app) => {
// Exclude current app // Exclude current app
if (app.name === currentAppName) return false; if (excludeCurrentApp && app.name === currentAppName) return false;
// Exclude already mentioned apps (case-insensitive comparison) // Exclude already mentioned apps (case-insensitive comparison)
if ( if (
@@ -185,7 +255,7 @@ export function LexicalChatInput({
return { return {
"@": appMentions, "@": appMentions,
}; };
}, [apps, selectedAppId, value]); }, [apps, selectedAppId, value, excludeCurrentApp]);
const initialConfig = { const initialConfig = {
namespace: "ChatInput", namespace: "ChatInput",
@@ -203,7 +273,6 @@ export function LexicalChatInput({
const root = $getRoot(); const root = $getRoot();
let textContent = root.getTextContent(); let textContent = root.getTextContent();
console.time("handleEditorChange");
// Transform @AppName mentions to @app:AppName format // Transform @AppName mentions to @app:AppName format
// This regex matches @AppName where AppName is one of our actual app names // 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"); textContent = textContent.replace(mentionRegex, "@app:$1");
} }
} }
console.timeEnd("handleEditorChange");
onChange(textContent); onChange(textContent);
}); });
}, },
@@ -275,6 +343,7 @@ export function LexicalChatInput({
<OnChangePlugin onChange={handleEditorChange} /> <OnChangePlugin onChange={handleEditorChange} />
<HistoryPlugin /> <HistoryPlugin />
<EnterKeyPlugin onSubmit={handleSubmit} /> <EnterKeyPlugin onSubmit={handleSubmit} />
<ExternalValueSyncPlugin value={value} />
<ClearEditorPlugin <ClearEditorPlugin
shouldClear={shouldClear} shouldClear={shouldClear}
onCleared={handleCleared} onCleared={handleCleared}

View File

@@ -1,11 +1,13 @@
export const MENTION_REGEX = /@app:([a-zA-Z0-9_-]+)/g;
// Helper function to parse app mentions from prompt // Helper function to parse app mentions from prompt
export function parseAppMentions(prompt: string): string[] { export function parseAppMentions(prompt: string): string[] {
// Match @app:AppName patterns in the prompt (supports letters, digits, underscores, and hyphens, but NOT spaces) // Match @app:AppName patterns in the prompt (supports letters, digits, underscores, and hyphens, but NOT spaces)
const mentionRegex = /@app:([a-zA-Z0-9_-]+)/g;
const mentions: string[] = []; const mentions: string[] = [];
let match; let match;
while ((match = mentionRegex.exec(prompt)) !== null) { while ((match = MENTION_REGEX.exec(prompt)) !== null) {
mentions.push(match[1]); mentions.push(match[1]);
} }