Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions src/ai-assistant/prompts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { editorsContent, AIConfig } from '../types/components/AIAssistant.types';

const APPLY_TAG_INSTRUCTIONS = `When you return code that is complete and ready to replace an editor, you must use one of these exact fenced code block tags:
- \`\`\`templatemarkApply for full TemplateMark replacements
- \`\`\`concertoApply for full Concerto replacements
- \`\`\`jsonApply for full JSON data replacements
Do not use plain templatemark, concerto, or json for full-file replacements.
If a change in one editor requires coordinated updates in another editor, return complete apply-ready blocks for every affected editor.`;

const includeEditorContents = (prompt: string, aiConfig: AIConfig | undefined, editorsContent: editorsContent) => {
if (!aiConfig?.includeTemplateMarkContent && !aiConfig?.includeConcertoModelContent && !aiConfig?.includeDataContent) {
return prompt;
Expand All @@ -25,12 +32,18 @@ const includeEditorContents = (prompt: string, aiConfig: AIConfig | undefined, e

export const prepareSystemPrompt = {
textToTemplate: (editorsContent: editorsContent, aiConfig?: AIConfig) => {
const prompt = `You are a helpful assistant that converts the following text into a valid Accord Project TemplateMark template without corresponding Concerto and JSON data.\n\n`;
const prompt = `You are a helpful assistant that converts the following text into a valid Accord Project TemplateMark template without corresponding Concerto and JSON data.
Return the final answer as a single complete TemplateMark replacement inside a \`\`\`templatemarkApply fenced code block.

${APPLY_TAG_INSTRUCTIONS}\n\n`;
return includeEditorContents(prompt, aiConfig, editorsContent);
},

createConcertoModel: (editorsContent: editorsContent, aiConfig?: AIConfig) => {
const prompt = `You are a helpful assistant that creates valid Accord Project Concerto models without corresponding TemplateMark and JSON data.\n\n`;
const prompt = `You are a helpful assistant that creates valid Accord Project Concerto models without corresponding TemplateMark and JSON data.
Return the final answer as a single complete Concerto replacement inside a \`\`\`concertoApply fenced code block.

${APPLY_TAG_INSTRUCTIONS}\n\n`;
return includeEditorContents(prompt, aiConfig, editorsContent);
},

Expand Down Expand Up @@ -74,7 +87,11 @@ export const prepareSystemPrompt = {
},

default: (editorsContent: editorsContent, aiConfig?: AIConfig) => {
const prompt = `You are a helpful assistant that answers questions about open source Accord Project. You assist the user in working with TemplateMark, Concerto models and JSON data. Code blocks returned by you should be enclosed in backticks, the language names that you can use after three backticks are- "concerto","templatemark" and "json", suffix 'Apply' to the language name if it is a complete code block that can be used to replace the corresponding editor content, precisely, concertoApply, templatemarkApply and jsonApply. You must always try to return complete code block that can be applied to the editors. Concerto code, TemplateMark code and JSON data are supplied to TemplateEngine to produce the final output. For instance, a data field that is not in Concerto data model can't be in JSON data and therefore can't be used in TemplateMark you generate. Analyze the JSON data and Concerto model (if provided) carefully, only the fields with simple data types (String, Integer etc.) present in concept annotated with @template decorator can be directly accessed anywhere in the template. Other complex data fields that have custom concept declaration in the Concerto model and are represented as nested fields in JSON data, can only be used within {{#clause conceptName}} {{concept_property_name}} {{/clause}} tags. Therefore, in most cases you have to create a scope using clause tag in TemplateMark to access properties defined under a concept in Concerto. For enumerating through a list you can create a scope to access the properties in list items via {{#olist listName}} {{instancePropertyName}} {{/olist}} or {{#ulist listName}} {{instancePropertyName}} {{/ulist}}. For TemplateMark code, there's no such thing as 'this' keyword within list scope. Optional fields shouldn't be wrapped in an if or with block to check for their availability e.g. if Concerto model has age as optional don't wrap it in if block in TemplateMark. You can also use Typescript within TemplateMark by enclosing the Typescript code in {{% %}}, you must write all of the Typescript code within a single line enclosed in a single pair of opening {{% and closing %}}. You may use Typescript to achieve an objective in TemplateMark only if TemplateMark syntax makes doing something hard, the data objects from JSON are readily available within {{% %}} enclosed Typescript using direct access, e.g. {{% return order.orderLines %}}. For e.g., you could use TypeScript to render ordered/unordered primitive list types such as String[]. Keep your focus on generating valid output based on current editors' contents but if you make a change that isn't compatible with the content of existing editors, you must return the full code for those editors as well. You mustn't add any placeholder in TemplateMark which isn't in Concerto model and JSON data unless you modify the Concerto and JSON data to have that field at the appropriate place.\n\n`;
const prompt = `You are a helpful assistant that answers questions about open source Accord Project. You assist the user in working with TemplateMark, Concerto models and JSON data.

${APPLY_TAG_INSTRUCTIONS}

You must prefer complete apply-ready replacements whenever you are suggesting code the user can paste back into an editor. Concerto code, TemplateMark code and JSON data are supplied to TemplateEngine to produce the final output. For instance, a data field that is not in Concerto data model can't be in JSON data and therefore can't be used in TemplateMark you generate. Analyze the JSON data and Concerto model (if provided) carefully, only the fields with simple data types (String, Integer etc.) present in concept annotated with @template decorator can be directly accessed anywhere in the template. Other complex data fields that have custom concept declaration in the Concerto model and are represented as nested fields in JSON data, can only be used within {{#clause conceptName}} {{concept_property_name}} {{/clause}} tags. Therefore, in most cases you have to create a scope using clause tag in TemplateMark to access properties defined under a concept in Concerto. For enumerating through a list you can create a scope to access the properties in list items via {{#olist listName}} {{instancePropertyName}} {{/olist}} or {{#ulist listName}} {{instancePropertyName}} {{/ulist}}. For TemplateMark code, there's no such thing as 'this' keyword within list scope. Optional fields shouldn't be wrapped in an if or with block to check for their availability e.g. if Concerto model has age as optional don't wrap it in if block in TemplateMark. You can also use Typescript within TemplateMark by enclosing the Typescript code in {{% %}}, you must write all of the Typescript code within a single line enclosed in a single pair of opening {{% and closing %}}. You may use Typescript to achieve an objective in TemplateMark only if TemplateMark syntax makes doing something hard, the data objects from JSON are readily available within {{% %}} enclosed Typescript using direct access, e.g. {{% return order.orderLines %}}. For e.g., you could use TypeScript to render ordered/unordered primitive list types such as String[]. Keep your focus on generating valid output based on current editors' contents but if you make a change that isn't compatible with the content of existing editors, you must return the full code for those editors as well. You mustn't add any placeholder in TemplateMark which isn't in Concerto model and JSON data unless you modify the Concerto and JSON data to have that field at the appropriate place.\n\n`;
return includeEditorContents(prompt, aiConfig, editorsContent);
}
};
};
152 changes: 140 additions & 12 deletions src/components/AIChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand All @@ -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);
Expand Down Expand Up @@ -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]);

Expand Down Expand Up @@ -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);
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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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 applyTarget = getApplyTarget(language);
const blockKey = `${messageId ?? "message"}:${i}:${language}:${code}`;

parts.push(
<div key={key++} className="relative mt-2 mb-2">
<pre className="bg-gray-800 text-gray-100 p-3 rounded-lg text-xs overflow-x-auto">
{code.trim()}
{code}
</pre>
{applyTarget && code && (
<div className="mt-2 flex items-center gap-3">
<button
type="button"
className={`rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${theme.applyButton} ${
applyingBlockKey === blockKey ? theme.applyButtonDisabled : ""
}`}
onClick={() => void handleApplyToEditor(language, code, blockKey)}
disabled={applyingBlockKey === blockKey}
aria-label={`Apply this ${language} code block to the editor`}
>
{applyingBlockKey === blockKey ? "Applying..." : "Apply to Editor"}
</button>
{applyFeedback?.blockKey === blockKey && (
<span
className={`text-xs ${
applyFeedback.type === "success"
? theme.applyFeedbackSuccess
: theme.applyFeedbackError
}`}
>
{applyFeedback.message}
</span>
)}
</div>
)}
</div>
);
} else if (i % 2 === 0 && segments[i]) {
Expand Down Expand Up @@ -350,7 +477,8 @@ export const AIChatPanel = () => {
? (aiConfig?.showFullPrompt ? (
`**System message:** ${chatState.messages[index-1].content}\n**User message:** ${message.content}`
) : message.content)
: message.content
: message.content,
message.id
)}
{message.role === 'assistant' &&
message.id === chatState.messages[chatState.messages.length - 1].id &&
Expand Down Expand Up @@ -605,4 +733,4 @@ export const AIChatPanel = () => {
</div>
</div>
);
}
}