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
Empty file.
3 changes: 1 addition & 2 deletions src/ai-assistant/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ export const prepareSystemPrompt = {
},

default: (editorsContent: editorsContent, aiConfig?: any) => {
let prompt = `You are a helpful assistant that answers questions about open source Accord Project. You assist the user
to work with TemplateMark, Concerto models and JSON data. Code blocks returned by you should enclosed in backticks\n\n`;
let 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`;
return includeEditorContents(prompt, aiConfig, editorsContent);
}
};
164 changes: 106 additions & 58 deletions src/components/AIChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import useAppStore from "../store/store";
import { sendMessage, stopMessage } from "../ai-assistant/chatRelay";
import CodeDiffPopup from "./CodeDiffPopup";

export const AIChatPanel = () => {
const [promptPreset, setPromptPreset] = useState<string | null>(null);
Expand All @@ -15,7 +16,21 @@ export const AIChatPanel = () => {
editorAgreementData: state.editorAgreementData,
}));

const { chatState, resetChat, aiConfig, setAIConfig, setAIConfigOpen, setAIChatOpen, textColor } = useAppStore.getState()
const {
chatState,
resetChat,
aiConfig,
setAIConfig,
setAIConfigOpen,
setAIChatOpen,
setEditorValue,
setTemplateMarkdown,
setEditorModelCto,
setModelCto,
setEditorAgreementData,
setData,
textColor
} = useAppStore.getState()

const latestMessageRef = useRef<HTMLDivElement>(null);

Expand Down Expand Up @@ -105,64 +120,88 @@ export const AIChatPanel = () => {
}
};

const renderMessageContent = (content: string) => {
if (!content || !content.includes('```')) {
console.log("content is", content);
return (
<div className="text-sm prose prose-sm break-all max-w-none">
<ReactMarkdown
components={{
code: ({ children, className }) => <code className={`bg-gray-200 p-1 rounded-md before:content-[''] after:content-[''] ${className}`}>{children}</code>,
}}>
{content}
</ReactMarkdown>
</div>
);
}
const [showDiffPopup, setShowDiffPopup] = useState(false);
const [diffCodeProps, setDiffCodeProps] = useState({
newCode: "",
currentCode: "",
language: "",
onApply: (_code: string) => {}
});

const parts = [];
let key = 0;

const segments = content.split('```');

if (segments[0]) {
parts.push(
<div className="text-sm prose prose-sm max-w-none" key={key++}>
<ReactMarkdown>
{segments[0]}
</ReactMarkdown>
</div>
);
const handleApplyCode = (code: string, language: string) => {
let currentCode = "";
let applyFunction = (_code: string) => {};

if (language === "concerto") {
currentCode = editorsContent.editorModelCto;
applyFunction = (code: string) => {
setEditorModelCto(code);
setModelCto(code);
};
} else if (language === "templatemark") {
currentCode = editorsContent.editorTemplateMark;
applyFunction = (code: string) => {
setEditorValue(code);
setTemplateMarkdown(code);
};
} else if (language === "json") {
currentCode = editorsContent.editorAgreementData;
applyFunction = (code: string) => {
setEditorAgreementData(code);
setData(code);
};
}

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);
}

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()}
</pre>
</div>
);
} else if (i % 2 === 0 && segments[i]) {
parts.push(
<div className="text-sm prose prose-sm max-w-none" key={key++}>
<ReactMarkdown>
{segments[i]}
</ReactMarkdown>
</div>
);
}

const normalize = (str: string) => str.trim();
if (normalize(currentCode) === normalize(code)) {
applyFunction(code);
return;
}

return parts;

setDiffCodeProps({
newCode: code,
currentCode,
language,
onApply: applyFunction
});
setShowDiffPopup(true);
};

const renderMessageContent = (content: string) => {
return (
<div className="text-sm prose prose-md break-all max-w-none">
<ReactMarkdown
components={{
code: ({ children, className }) => {
if (className === undefined) return <code className={`rounded-md`}>{children}</code>;
let language = ((className as string).match(/language-[^ ]*/)?.toString())?.split("-")[1] || "";
const isApplicable = ["concertoApply", "templatemarkApply", "jsonApply"].includes(language);
language = language.slice(0, -5)
const codeContent = children?.toString() || "";

return (
<div className="relative">
{isApplicable && (
<button
onClick={() => handleApplyCode(codeContent, language)}
className="sticky top-2 right-2 bg-blue-500 text-white px-2 py-1 rounded text-xs hover:bg-blue-600 transition-colors flex items-center"
title={`Apply to ${language} editor`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 14L4 9L9 4" />
<path d="M4 9H16C18.2091 9 20 10.7909 20 13C20 15.2091 18.2091 17 16 17H12" />
</svg>
</button>
)}
<code className={`rounded-md ${className} block p-4`}>{children}</code>
</div>
);
}
}}>
{content}
</ReactMarkdown>
</div>
);
};

useEffect(() => {
Expand Down Expand Up @@ -252,7 +291,7 @@ export const AIChatPanel = () => {
</button>
</div>
</div>
<div className="w-full h-[calc(100%-3rem)] flex flex-col">
<div className="w-full h-[calc(100%-1rem)] flex flex-col">
<div className="flex-1 overflow-y-auto mb-4 px-2 mt-4">
<div className="space-y-2">
{chatState.messages.length === 0 ? (
Expand Down Expand Up @@ -303,7 +342,7 @@ export const AIChatPanel = () => {
<div className="flex flex-col gap-2">
{promptPreset && (
<div className="inline-flex items-center gap-2 px-3 py-1.5 bg-indigo-600 bg-opacity-95 text-white rounded-lg self-start">
<span className="text-sm font-medium prose lg:prose-md">
<span className="text-sm font-medium">
{
promptPreset === "textToTemplate" ? "Text to TemplateMark" : "Create Concerto Model"
}
Expand Down Expand Up @@ -534,6 +573,15 @@ export const AIChatPanel = () => {
</div>
</div>
</div>
{showDiffPopup && (
<CodeDiffPopup
newCode={diffCodeProps.newCode}
currentCode={diffCodeProps.currentCode}
language={diffCodeProps.language}
onApply={diffCodeProps.onApply}
onClose={() => setShowDiffPopup(false)}
/>
)}
</div>
);
}
127 changes: 127 additions & 0 deletions src/components/CodeDiffPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React, { useState, useMemo, lazy, Suspense } from 'react';
import { useMonaco } from '@monaco-editor/react';
import { CodeDiffPopupProps } from "../types/components/AIAssistant.types"
import useAppStore from '../store/store';

const MonacoDiffEditor = lazy(() =>
import("@monaco-editor/react").then(mod => ({ default: mod.DiffEditor }))
);

const CodeDiffPopup: React.FC<CodeDiffPopupProps> = ({
newCode,
currentCode,
language,
onApply,
onClose,
}) => {
language = (language === 'templatemark' ? 'markdown' : language);
const monaco = useMonaco();
const [modifiedContent, setModifiedContent] = useState(newCode);

const {backgroundColor} = useAppStore((state) => ({
backgroundColor: state.backgroundColor,
}));
const isDarkMode = useMemo(() =>
backgroundColor && backgroundColor !== '#ffffff',
[backgroundColor]);
const theme = useMemo(() => ({
popupBg: isDarkMode ? 'bg-gray-900' : 'bg-white',
popupBorder: isDarkMode ? 'border-gray-700' : 'border-gray-300',
headerBg: isDarkMode ? 'bg-gray-800' : 'bg-white',
headerText: isDarkMode ? 'text-gray-100' : 'text-gray-900',
subHeaderBg: isDarkMode ? 'bg-gray-800' : 'bg-gray-100',
subHeaderText: isDarkMode ? 'text-gray-400' : 'text-gray-600',
buttonBg: isDarkMode ? 'bg-gray-700' : 'bg-gray-200',
buttonBgHover: isDarkMode ? 'bg-gray-600' : 'bg-gray-300',
buttonText: isDarkMode ? 'text-gray-100' : 'text-gray-900',
closeButtonText: isDarkMode ? 'text-gray-400' : 'text-gray-500',
closeButtonHover: isDarkMode ? 'hover:text-gray-200' : 'hover:text-gray-800',
applyButtonBg: isDarkMode ? 'bg-blue-700' : 'bg-blue-500',
applyButtonBgHover: isDarkMode ? 'bg-blue-800' : 'bg-blue-600',
applyButtonText: 'text-white',
border: isDarkMode ? 'border border-gray-700' : 'border border-gray-300',
}), [isDarkMode]);
const themeName = useMemo(
() => (isDarkMode ? "darkTheme" : "lightTheme"),
[isDarkMode]
);

const diffEditorOptions = useMemo(() => ({
originalEditable: false,
readOnly: false,
renderSideBySide: true,
enableSplitViewResizing: false,
renderIndicators: true,
folding: false,
minimap: { enabled: false },
scrollBeyondLastLine: false,
ignoreTrimWhitespace: false,
glyphMargin: true,
}), []);

React.useEffect(() => {
setModifiedContent(newCode);
}, [newCode]);

if (!monaco) {
return (
<div className={`fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50`}>
<div className={`rounded-lg w-4/5 max-w-4xl max-h-[80vh] flex flex-col ${theme.popupBg} ${theme.popupBorder}`} style={{height: '80vh'}}>
<div className="flex items-center justify-center h-full">
<div className={`${theme.headerText}`}>Loading Monaco Editor...</div>
</div>
</div>
</div>
);
}

return (
<div className={`fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50`}>
<div className={`rounded-lg w-4/5 max-w-4xl max-h-[80vh] flex flex-col ${theme.popupBg} ${theme.popupBorder}`} style={{height: '80vh'}}>
<div className={`flex-shrink-0 flex justify-between items-center p-4 border-b ${theme.headerBg} ${theme.popupBorder}`}>
<h3 className={`font-bold ${theme.headerText}`}>Apply Code Changes ({language})</h3>
<button onClick={onClose} className={`${theme.closeButtonText} ${theme.closeButtonHover}`}>
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>

<div className="flex-1 flex flex-col min-h-0" style={{height: '100%'}}>
<div className={`flex-shrink-0 p-2 text-sm border-b ${theme.subHeaderBg} ${theme.popupBorder}`}>
<div className={`text-xs ${theme.subHeaderText}`}>
Review and edit the proposed changes below.
</div>
</div>
<Suspense fallback={<div>Loading Diff Editor...</div>}>
<MonacoDiffEditor
height="100%"
language={language}
theme={themeName}
options={diffEditorOptions}
original={currentCode}
modified={modifiedContent}
/>
</Suspense>
</div>

<div className={`flex-shrink-0 p-4 border-t flex justify-end gap-2 ${theme.popupBorder}`}>
<button
onClick={onClose}
className={`px-4 py-2 rounded ${theme.buttonBg} ${theme.buttonText} hover:${theme.buttonBgHover}`}
>
Cancel
</button>
<button
onClick={() => { onApply(modifiedContent); onClose(); }}
className={`px-4 py-2 rounded ${theme.applyButtonBg} ${theme.applyButtonText} hover:${theme.applyButtonBgHover}`}
>
Apply Changes
</button>
</div>
</div>
</div>
);
};

export default CodeDiffPopup;
Loading