-
-
Notifications
You must be signed in to change notification settings - Fork 222
feat(ai): add apply-to-editor actions for assistant code suggestions #864
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,11 +2,22 @@ import { useEffect, useRef, useState, useMemo } from "react"; | |||||
| import ReactMarkdown from "react-markdown"; | ||||||
| import useAppStore from "../store/store"; | ||||||
| import { sendMessage, stopMessage } from "../ai-assistant/chatRelay"; | ||||||
| import { updateEditorActivity } from "../ai-assistant/activityTracker"; | ||||||
|
|
||||||
| type ApplyLanguage = "concertoapply" | "templatemarkapply" | "jsonapply"; | ||||||
|
|
||||||
| type ApplyFeedback = { | ||||||
| blockKey: string; | ||||||
| message: string; | ||||||
| type: "error" | "success"; | ||||||
| }; | ||||||
|
|
||||||
| export const AIChatPanel = () => { | ||||||
| const [promptPreset, setPromptPreset] = useState<string | null>(null); | ||||||
| const [isDropdownOpen, setIsDropdownOpen] = useState(false); | ||||||
| const [userInput, setUserInput] = useState(""); | ||||||
| const [applyFeedback, setApplyFeedback] = useState<ApplyFeedback | null>(null); | ||||||
| const [applyingBlockKey, setApplyingBlockKey] = useState<string | null>(null); | ||||||
| const textareaRef = useRef<HTMLTextAreaElement>(null); | ||||||
|
|
||||||
| const editorsContent = useAppStore((state) => ({ | ||||||
|
|
@@ -15,15 +26,36 @@ export const AIChatPanel = () => { | |||||
| editorAgreementData: state.editorAgreementData, | ||||||
| })); | ||||||
|
|
||||||
| const { chatState, resetChat, aiConfig, setAIConfig, setAIConfigOpen, setAIChatOpen, textColor, backgroundColor } = useAppStore((state) => ({ | ||||||
| const { | ||||||
| chatState, | ||||||
| resetChat, | ||||||
| aiConfig, | ||||||
| setAIConfig, | ||||||
| setAIConfigOpen, | ||||||
| setAIChatOpen, | ||||||
| textColor, | ||||||
| backgroundColor, | ||||||
| setEditorValue, | ||||||
| setTemplateMarkdown, | ||||||
| setEditorModelCto, | ||||||
| setModelCto, | ||||||
| setEditorAgreementData, | ||||||
| setData | ||||||
| } = useAppStore((state) => ({ | ||||||
| chatState: state.chatState, | ||||||
| resetChat: state.resetChat, | ||||||
| aiConfig: state.aiConfig, | ||||||
| setAIConfig: state.setAIConfig, | ||||||
| setAIConfigOpen: state.setAIConfigOpen, | ||||||
| setAIChatOpen: state.setAIChatOpen, | ||||||
| textColor: state.textColor, | ||||||
| backgroundColor: state.backgroundColor | ||||||
| backgroundColor: state.backgroundColor, | ||||||
| setEditorValue: state.setEditorValue, | ||||||
| setTemplateMarkdown: state.setTemplateMarkdown, | ||||||
| setEditorModelCto: state.setEditorModelCto, | ||||||
| setModelCto: state.setModelCto, | ||||||
| setEditorAgreementData: state.setEditorAgreementData, | ||||||
| setData: state.setData | ||||||
| })); | ||||||
|
|
||||||
| const latestMessageRef = useRef<HTMLDivElement>(null); | ||||||
|
|
@@ -68,7 +100,13 @@ export const AIChatPanel = () => { | |||||
| } | ||||||
| }, | ||||||
|
|
||||||
| inlineCode: isDarkMode ? 'bg-gray-700 text-gray-200' : 'bg-gray-200 text-gray-800' | ||||||
| inlineCode: isDarkMode ? 'bg-gray-700 text-gray-200' : 'bg-gray-200 text-gray-800', | ||||||
| applyButton: isDarkMode | ||||||
| ? 'bg-blue-800 text-blue-100 hover:bg-blue-700' | ||||||
| : 'bg-blue-600 text-white hover:bg-blue-700', | ||||||
| applyButtonDisabled: 'opacity-60 cursor-not-allowed', | ||||||
| applyFeedbackSuccess: isDarkMode ? 'text-green-300' : 'text-green-700', | ||||||
| applyFeedbackError: isDarkMode ? 'text-red-300' : 'text-red-700' | ||||||
| }; | ||||||
| }, [backgroundColor]); | ||||||
|
|
||||||
|
|
@@ -148,6 +186,70 @@ export const AIChatPanel = () => { | |||||
| stopMessage(); | ||||||
| }; | ||||||
|
|
||||||
| const getApplyTarget = (language: string) => { | ||||||
| const normalizedLanguage = language.toLowerCase() as ApplyLanguage; | ||||||
|
|
||||||
| switch (normalizedLanguage) { | ||||||
| case "concertoapply": | ||||||
| return { | ||||||
| apply: async (code: string) => { | ||||||
| updateEditorActivity("concerto"); | ||||||
| setEditorModelCto(code); | ||||||
| await setModelCto(code); | ||||||
| }, | ||||||
| successMessage: "Applied to the Concerto editor and rebuilt preview.", | ||||||
| }; | ||||||
| case "templatemarkapply": | ||||||
| return { | ||||||
| apply: async (code: string) => { | ||||||
| updateEditorActivity("markdown"); | ||||||
| setEditorValue(code); | ||||||
| await setTemplateMarkdown(code); | ||||||
| }, | ||||||
| successMessage: "Applied to the TemplateMark editor and rebuilt preview.", | ||||||
| }; | ||||||
| case "jsonapply": | ||||||
| return { | ||||||
| apply: async (code: string) => { | ||||||
| updateEditorActivity("json"); | ||||||
| setEditorAgreementData(code); | ||||||
| await setData(code); | ||||||
| }, | ||||||
| successMessage: "Applied to the JSON data editor and rebuilt preview.", | ||||||
| }; | ||||||
| default: | ||||||
| return null; | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| const handleApplyToEditor = async (language: string, code: string, blockKey: string) => { | ||||||
| const applyTarget = getApplyTarget(language); | ||||||
|
|
||||||
| if (!applyTarget) { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| setApplyingBlockKey(blockKey); | ||||||
| setApplyFeedback(null); | ||||||
|
|
||||||
| try { | ||||||
| await applyTarget.apply(code); | ||||||
|
Comment on lines
+232
to
+236
|
||||||
| setApplyFeedback({ | ||||||
| blockKey, | ||||||
| message: applyTarget.successMessage, | ||||||
| type: "success", | ||||||
| }); | ||||||
| } catch (error) { | ||||||
| setApplyFeedback({ | ||||||
| blockKey, | ||||||
| message: error instanceof Error ? error.message : "Could not apply this suggestion.", | ||||||
| type: "error", | ||||||
| }); | ||||||
| } finally { | ||||||
| setApplyingBlockKey(null); | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| const handleKeyDown = (e: React.KeyboardEvent) => { | ||||||
| if (e.key === "Enter" && !e.shiftKey) { | ||||||
| if (chatState.isLoading) { | ||||||
|
|
@@ -158,7 +260,7 @@ export const AIChatPanel = () => { | |||||
| } | ||||||
| }; | ||||||
|
|
||||||
| const renderMessageContent = (content: string) => { | ||||||
| const renderMessageContent = (content: string, messageId?: string) => { | ||||||
| // Detect error marker | ||||||
| const isError = content.startsWith('[ERROR]'); | ||||||
| const displayContent = isError ? content.replace(/^\[ERROR\]\s*/, '') : content; | ||||||
|
|
@@ -194,17 +296,42 @@ export const AIChatPanel = () => { | |||||
| for (let i = 1; i < segments.length; i++) { | ||||||
| if (i % 2 === 1 && segments[i]) { | ||||||
| const firstLineBreak = segments[i].indexOf('\n'); | ||||||
| let code = segments[i]; | ||||||
|
|
||||||
| if (firstLineBreak > -1) { | ||||||
| code = segments[i].substring(firstLineBreak + 1); | ||||||
| } | ||||||
| const language = firstLineBreak > -1 ? segments[i].substring(0, firstLineBreak).trim() : ""; | ||||||
| const code = (firstLineBreak > -1 ? segments[i].substring(firstLineBreak + 1) : segments[i]).trim(); | ||||||
|
||||||
| const code = (firstLineBreak > -1 ? segments[i].substring(firstLineBreak + 1) : segments[i]).trim(); | |
| const code = firstLineBreak > -1 ? segments[i].substring(firstLineBreak + 1) : segments[i]; |
Copilot
AI
Apr 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
blockKey includes the entire code string, which can be very large. This increases memory usage and makes state comparisons (applyingBlockKey === blockKey) potentially expensive; it also risks unstable keys if whitespace changes. Use a stable, short identifier (e.g., ${messageId}:${i}:${language}) and, if needed, keep the code separately.
| const blockKey = `${messageId ?? "message"}:${i}:${language}:${code}`; | |
| const blockKey = `${messageId ?? "message"}:${i}:${language}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR adds a new user-facing workflow (detect *Apply code fences, render an Apply button, update the correct editor, and rebuild). There are component tests in
src/tests/components/, but no coverage for AIChatPanel. Please add unit tests for at least: (1) rendering the Apply button only fortemplatemarkApply/concertoApply/jsonApplyblocks, and (2) clicking Apply calling the correct store setters and showing success/error feedback.