@@ -39,4 +41,4 @@ function AgreementData() {
);
}
-export default AgreementData;
\ No newline at end of file
+export default AgreementData;
diff --git a/src/editors/editorsContainer/TemplateMarkdown.tsx b/src/editors/editorsContainer/TemplateMarkdown.tsx
index 28e91af..80daf1f 100644
--- a/src/editors/editorsContainer/TemplateMarkdown.tsx
+++ b/src/editors/editorsContainer/TemplateMarkdown.tsx
@@ -2,6 +2,7 @@ import MarkdownEditor from "../MarkdownEditor";
import useAppStore from "../../store/store";
import useUndoRedo from "../../components/useUndoRedo";
import { FaUndo, FaRedo } from "react-icons/fa";
+import { AIButton } from "../../components/AIAssistant/AIButton";
function TemplateMarkdown() {
const textColor = useAppStore((state) => state.textColor);
@@ -18,7 +19,7 @@ function TemplateMarkdown() {
const handleChange = (value: string | undefined) => {
if (value !== undefined) {
setValue(value); // Update editor state and sync
- setTemplateMarkdown(value);
+ setTemplateMarkdown(value);
}
};
@@ -27,6 +28,7 @@ function TemplateMarkdown() {
TemplateMark
@@ -39,4 +41,4 @@ function TemplateMarkdown() {
);
}
-export default TemplateMarkdown;
\ No newline at end of file
+export default TemplateMarkdown;
diff --git a/src/editors/editorsContainer/TemplateModel.tsx b/src/editors/editorsContainer/TemplateModel.tsx
index ca4f0fc..415e073 100644
--- a/src/editors/editorsContainer/TemplateModel.tsx
+++ b/src/editors/editorsContainer/TemplateModel.tsx
@@ -3,6 +3,7 @@ import useAppStore from "../../store/store";
import useUndoRedo from "../../components/useUndoRedo";
import { FaUndo, FaRedo } from "react-icons/fa";
+import { AIButton } from "../../components/AIAssistant/AIButton";
function TemplateModel() {
const textColor = useAppStore((state) => state.textColor);
@@ -14,11 +15,11 @@ function TemplateModel() {
setEditorModelCto,
setModelCto // Sync to main state and rebuild
);
-
+
const handleChange = (value: string | undefined) => {
if (value !== undefined) {
setValue(value); // Update editor state and sync
- setModelCto(value);
+ setModelCto(value);
}
};
@@ -27,6 +28,7 @@ function TemplateModel() {
Concerto Model
@@ -39,4 +41,4 @@ function TemplateModel() {
);
}
-export default TemplateModel;
\ No newline at end of file
+export default TemplateModel;
diff --git a/src/hooks/useAI.ts b/src/hooks/useAI.ts
new file mode 100644
index 0000000..d2ac4e7
--- /dev/null
+++ b/src/hooks/useAI.ts
@@ -0,0 +1,31 @@
+import { useCallback } from "react";
+import { type EditorType, type AICompletion } from "../services/ai/AIService";
+import { useAIStore } from "../store/aistore";
+
+export function useAI() {
+ const { isProcessing, generateContent } = useAIStore();
+
+ const getCompletion = useCallback(
+ async ({
+ prompt,
+ editorType,
+ currentContent,
+ }: {
+ prompt: string;
+ editorType: EditorType;
+ currentContent?: string;
+ }): Promise
=> {
+ return await generateContent({
+ prompt,
+ editorType,
+ currentContent,
+ });
+ },
+ [generateContent]
+ );
+
+ return {
+ isProcessing,
+ getCompletion,
+ };
+}
diff --git a/src/services/ai/AIService.ts b/src/services/ai/AIService.ts
new file mode 100644
index 0000000..126eeb4
--- /dev/null
+++ b/src/services/ai/AIService.ts
@@ -0,0 +1,145 @@
+import { HuggingFaceAPI } from "./HuggingFaceAPI";
+import { GeminiAPI } from "./GeminiAPI";
+
+import { TEMPLATEMARK_SYSTEM_PROMPT, TEMPLATEMARK_EXAMPLES } from "./prompts/templateMark";
+import { CONCERTO_SYSTEM_PROMPT, CONCERTO_EXAMPLES } from "./prompts/concerto";
+import { DATA_SYSTEM_PROMPT, DATA_EXAMPLES } from "./prompts/data";
+
+import { AI_CONFIG } from "../../config/ai.config";
+
+export type EditorType = "templatemark" | "concerto" | "data";
+
+export interface AIRequest {
+ prompt: string;
+ editorType: EditorType;
+ currentContent?: string;
+}
+
+export interface AICompletion {
+ content: string;
+ diff?: string;
+ explanation?: string;
+}
+
+export class AIService {
+ private huggingFaceAPI: HuggingFaceAPI;
+ private geminiAPI: GeminiAPI;
+
+ constructor() {
+ this.huggingFaceAPI = new HuggingFaceAPI(AI_CONFIG.huggingfaceApiKey || "");
+ this.geminiAPI = new GeminiAPI(AI_CONFIG.geminiApiKey || "");
+ }
+
+ async getCompletion({ prompt, editorType, currentContent }: AIRequest): Promise {
+ try {
+ const result = await this.getHuggingFaceCompletion({ prompt, editorType, currentContent });
+ return this.processCompletion(result, currentContent);
+ } catch (error: any) {
+ console.log("Hugging Face API error, trying Gemini:", error.message);
+ try {
+ const result = await this.getGeminiCompletion({ prompt, editorType, currentContent });
+ return this.processCompletion(result, currentContent);
+ } catch (geminiError: any) {
+ console.error("All AI providers failed:", geminiError.message);
+ return {
+ content: "Could not generate completion. Please try again.",
+ explanation: "All AI providers failed to generate a response.",
+ };
+ }
+ }
+ }
+
+ private processCompletion(generatedText: string, currentContent?: string): AICompletion {
+ // vinyl: extract comments that might be explanations
+ const explanationRegex = /\/\*\*([\s\S]*?)\*\//g;
+ const explanationMatches = [...generatedText.matchAll(explanationRegex)];
+ let explanation = "";
+
+ if (explanationMatches.length > 0) {
+ explanation = explanationMatches.map((match) => match[1].trim()).join("\n");
+ // vinyl: remove explanations from the content
+ generatedText = generatedText.replace(explanationRegex, "");
+ }
+
+ if (!currentContent) {
+ return {
+ content: generatedText.trim(),
+ explanation,
+ };
+ }
+
+ const diff = this.generateDiff(currentContent, generatedText);
+
+ return {
+ content: generatedText.trim(),
+ diff,
+ explanation,
+ };
+ }
+
+ private generateDiff(oldContent: string, newContent: string): string {
+ const oldLines = oldContent.split("\n");
+ const newLines = newContent.split("\n");
+ let diffText = "";
+
+ for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) {
+ const oldLine = i < oldLines.length ? oldLines[i] : "";
+ const newLine = i < newLines.length ? newLines[i] : "";
+
+ if (oldLine !== newLine) {
+ if (oldLine) {
+ diffText += `- ${oldLine}\n`;
+ }
+ if (newLine) {
+ diffText += `+ ${newLine}\n`;
+ }
+ }
+ }
+
+ return diffText;
+ }
+
+ private async getHuggingFaceCompletion({ prompt, editorType, currentContent }: AIRequest): Promise {
+ return await this.huggingFaceAPI.getCompletion({
+ prompt,
+ currentContent,
+ editorType,
+ });
+ }
+
+ private async getGeminiCompletion({ prompt, editorType, currentContent }: AIRequest): Promise {
+ return await this.geminiAPI.getCompletion({
+ prompt,
+ currentContent,
+ editorType,
+ systemPrompt: this.getSystemPrompt(editorType),
+ examples: this.getExamples(editorType),
+ });
+ }
+
+ private getSystemPrompt(editorType: EditorType): string {
+ switch (editorType) {
+ case "templatemark":
+ return TEMPLATEMARK_SYSTEM_PROMPT;
+ case "concerto":
+ return CONCERTO_SYSTEM_PROMPT;
+ case "data":
+ return DATA_SYSTEM_PROMPT;
+ default:
+ return "";
+ }
+ }
+
+ private getExamples(editorType: EditorType): Array<{ role: string; content: string }> {
+ switch (editorType) {
+ case "templatemark":
+ return TEMPLATEMARK_EXAMPLES;
+ case "concerto":
+ return CONCERTO_EXAMPLES;
+ case "data":
+ return DATA_EXAMPLES;
+ default:
+ return [];
+ }
+ }
+}
diff --git a/src/services/ai/GeminiAPI.ts b/src/services/ai/GeminiAPI.ts
new file mode 100644
index 0000000..3faf4bc
--- /dev/null
+++ b/src/services/ai/GeminiAPI.ts
@@ -0,0 +1,85 @@
+export class GeminiAPI {
+ constructor(private apiKey: string) {}
+
+ async getCompletion({
+ prompt,
+ currentContent,
+ editorType,
+ systemPrompt,
+ examples,
+ }: {
+ prompt: string;
+ currentContent?: string;
+ editorType?: string;
+ systemPrompt?: string;
+ examples?: Array<{ role: string; content: string }>;
+ }): Promise {
+ if (!this.apiKey) {
+ throw new Error("Gemini API key is not configured");
+ }
+
+ try {
+ const formattedPrompt = this.formatPrompt(prompt, currentContent, editorType, systemPrompt);
+
+ const contents = examples
+ ? [
+ ...examples.map((ex) => ({ role: ex.role, parts: [{ text: ex.content }] })),
+ { role: "user", parts: [{ text: formattedPrompt }] },
+ ]
+ : [{ role: "user", parts: [{ text: formattedPrompt }] }];
+
+ const response = await fetch(
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${this.apiKey}`,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ contents,
+ generationConfig: {
+ temperature: 0.7,
+ maxOutputTokens: 2000,
+ topP: 0.95,
+ topK: 40,
+ },
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`Gemini API error: ${response.statusText}`);
+ }
+
+ const result = await response.json();
+
+ if (
+ result.candidates &&
+ result.candidates.length > 0 &&
+ result.candidates[0].content &&
+ result.candidates[0].content.parts &&
+ result.candidates[0].content.parts.length > 0
+ ) {
+ return result.candidates[0].content.parts[0].text;
+ }
+
+ throw new Error("Unexpected response format from Gemini API");
+ } catch (error) {
+ console.error("Gemini API error:", error);
+ throw error;
+ }
+ }
+
+ private formatPrompt(prompt: string, currentContent?: string, editorType?: string, systemPrompt?: string): string {
+ const systemPrompts = {
+ templatemark: "Create a TemplateMark document following this format:\n",
+ concerto: "Create a Concerto model with these specifications:\n",
+ data: "Generate JSON data according to this structure:\n",
+ };
+
+ const specificSystemPrompt = systemPrompt || systemPrompts[editorType as keyof typeof systemPrompts] || "";
+
+ return `${specificSystemPrompt}
+ ${currentContent ? `Current content:\n${currentContent}\n\n` : ""}
+ Request: ${prompt}
+ `;
+ }
+}
diff --git a/src/services/ai/HuggingFaceAPI.ts b/src/services/ai/HuggingFaceAPI.ts
new file mode 100644
index 0000000..320f234
--- /dev/null
+++ b/src/services/ai/HuggingFaceAPI.ts
@@ -0,0 +1,97 @@
+import { AI_CONFIG } from "../../config/ai.config";
+
+export class HuggingFaceAPI {
+ constructor(private apiKey: string) {}
+
+ async getCompletion({
+ prompt,
+ currentContent,
+ editorType,
+ }: {
+ prompt: string;
+ currentContent?: string;
+ editorType?: string;
+ }): Promise {
+ // vinyl: default primary model
+ const primaryModel = AI_CONFIG.huggingFaceModels.primary;
+
+ try {
+ return await this.callModel(primaryModel, prompt, currentContent, editorType);
+ } catch (error) {
+ console.error("Primary model error, trying fallback:", error);
+ try {
+ return await this.fallbackCompletion(prompt, currentContent, editorType);
+ } catch (fallbackError) {
+ console.error("First fallback failed, trying second fallback:", fallbackError);
+ return await this.secondFallbackCompletion(prompt, currentContent, editorType);
+ }
+ }
+ }
+
+ private async callModel(
+ model: string,
+ prompt: string,
+ currentContent?: string,
+ editorType?: string
+ ): Promise {
+ const response = await fetch(`https://api-inference.huggingface.co/models/${model}`, {
+ headers: {
+ Authorization: `Bearer ${this.apiKey}`,
+ "Content-Type": "application/json",
+ },
+ method: "POST",
+ body: JSON.stringify({
+ inputs: this.formatPrompt(prompt, currentContent, editorType),
+ parameters: {
+ max_length: 2000,
+ temperature: 0.7,
+ num_return_sequences: 1,
+ top_k: 50,
+ },
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Hugging Face API error: ${response.statusText}`);
+ }
+
+ const result = await response.json();
+ if (Array.isArray(result)) {
+ return result[0]?.generated_text || "";
+ } else if (typeof result === "object" && result.generated_text) {
+ return result.generated_text;
+ } else if (typeof result === "string") {
+ return result;
+ }
+
+ throw new Error("Unexpected response format from Hugging Face API");
+ }
+
+ private async fallbackCompletion(prompt: string, currentContent?: string, editorType?: string): Promise {
+ const fallbackModel = AI_CONFIG.huggingFaceModels.fallback1;
+ return await this.callModel(fallbackModel, prompt, currentContent, editorType);
+ }
+
+ private async secondFallbackCompletion(
+ prompt: string,
+ currentContent?: string,
+ editorType?: string
+ ): Promise {
+ const fallbackModel = AI_CONFIG.huggingFaceModels.fallback2;
+ return await this.callModel(fallbackModel, prompt, currentContent, editorType);
+ }
+
+ private formatPrompt(prompt: string, currentContent?: string, editorType?: string): string {
+ const systemPrompts = {
+ templatemark: "Create a TemplateMark document following this format:\n",
+ concerto: "Create a Concerto model with these specifications:\n",
+ data: "Generate JSON data according to this structure:\n",
+ };
+
+ return `${systemPrompts[editorType as keyof typeof systemPrompts] || ""}
+${currentContent ? `Current content:\n${currentContent}\n\n` : ""}
+Request: ${prompt}
+
+`;
+ }
+}
diff --git a/src/services/ai/prompts/concerto.ts b/src/services/ai/prompts/concerto.ts
new file mode 100644
index 0000000..92b3844
--- /dev/null
+++ b/src/services/ai/prompts/concerto.ts
@@ -0,0 +1,33 @@
+export const CONCERTO_SYSTEM_PROMPT = `You are an expert in Accord Project Concerto modeling language. Help create and modify models following these conventions:
+
+1. Models must have a namespace
+2. Use @template decorator for template concepts
+3. Properties use 'o' prefix
+4. Follow Concerto naming conventions
+
+Example model structure:
+namespace org.example@1.0.0
+
+@template
+concept TemplateConcept {
+ o String property1
+ o Integer property2 optional
+}`;
+
+export const CONCERTO_EXAMPLES = [
+ {
+ role: "user",
+ content: "Create a model for a product announcement",
+ },
+ {
+ role: "assistant",
+ content: `namespace org.example@1.0.0
+
+@template
+concept ProductAnnouncement {
+ o String productName
+ o String description
+ o DateTime releaseDate
+}`,
+ },
+];
diff --git a/src/services/ai/prompts/data.ts b/src/services/ai/prompts/data.ts
new file mode 100644
index 0000000..ac66054
--- /dev/null
+++ b/src/services/ai/prompts/data.ts
@@ -0,0 +1,77 @@
+export const DATA_SYSTEM_PROMPT = `You are an expert in creating JSON data for Accord Project templates. Help create and modify data following these conventions:
+
+1. Data must match the Concerto model structure
+2. Use $class property to specify the fully qualified type
+3. Include all required properties
+4. Format dates as ISO strings
+5. Nested objects should have their own $class if they are concept instances
+
+Example data structure:
+{
+ "$class": "org.example@1.0.0.TemplateConcept",
+ "property1": "value",
+ "property2": 42,
+ "nestedObject": {
+ "$class": "org.example@1.0.0.NestedConcept",
+ "nestedProperty": "value"
+ }
+}`;
+
+export const DATA_EXAMPLES = [
+ {
+ role: "user",
+ content:
+ "Create sample data for a product announcement template with fields for productName, description, and releaseDate",
+ },
+ {
+ role: "assistant",
+ content: `{
+ "$class": "org.example@1.0.0.ProductAnnouncement",
+ "productName": "New Product X",
+ "description": "Check out our latest product offering, featuring advanced features and improved performance.",
+ "releaseDate": "2024-10-01T00:00:00Z"
+}`,
+ },
+ {
+ role: "user",
+ content: "Create sample data for a customer order with name, address, age, salary, and order details",
+ },
+ {
+ role: "assistant",
+ content: `{
+ "$class": "org.example@1.0.0.TemplateData",
+ "name": "Jane Smith",
+ "address": {
+ "line1": "123 Main Street",
+ "city": "New York",
+ "state": "NY",
+ "country": "USA"
+ },
+ "age": 35,
+ "salary": {
+ "$class": "org.accordproject.money@0.3.0.MonetaryAmount",
+ "doubleValue": 2500,
+ "currencyCode": "USD"
+ },
+ "favoriteColors": ["blue", "purple", "teal"],
+ "order": {
+ "$class": "org.example@1.0.0.Order",
+ "createdAt": "2023-12-15T10:30:00Z",
+ "orderLines": [
+ {
+ "$class": "org.example@1.0.0.OrderLine",
+ "sku": "PROD-001",
+ "quantity": 2,
+ "price": 49.99
+ },
+ {
+ "$class": "org.example@1.0.0.OrderLine",
+ "sku": "PROD-002",
+ "quantity": 1,
+ "price": 29.99
+ }
+ ]
+ }
+}`,
+ },
+];
diff --git a/src/services/ai/prompts/templateMark.ts b/src/services/ai/prompts/templateMark.ts
new file mode 100644
index 0000000..5af5a46
--- /dev/null
+++ b/src/services/ai/prompts/templateMark.ts
@@ -0,0 +1,37 @@
+export const TEMPLATEMARK_SYSTEM_PROMPT = `You are an expert in Accord Project TemplateMark. Help create and modify templates following these conventions:
+
+1. Templates must use valid TemplateMark syntax
+2. Variables are referenced using {{variableName}}
+3. Clauses use {{#clause variableName}} syntax
+4. Formulas use {{% return expression %}} syntax
+
+Example template structure:
+> Template description
+
+### Title {{variable}}
+
+{{#clause object}}
+Content with {{nested.variables}}
+{{/clause}}
+
+Formula: {{% return calculation %}}`;
+
+export const TEMPLATEMARK_EXAMPLES = [
+ {
+ role: "user",
+ content: "Create a template for a product announcement",
+ },
+ {
+ role: "assistant",
+ content: `Here's a product announcement template:
+
+> Product announcement template.
+
+### Introducing {{productName}}
+
+{{description}}
+
+**Release Date:** {{releaseDate as "D MMMM YYYY"}}
+`,
+ },
+];
diff --git a/src/store/aistore.ts b/src/store/aistore.ts
new file mode 100644
index 0000000..63b35f3
--- /dev/null
+++ b/src/store/aistore.ts
@@ -0,0 +1,40 @@
+import { create } from "zustand";
+import { AIService, type EditorType, type AICompletion } from "../services/ai/AIService";
+
+interface AIState {
+ isProcessing: boolean;
+ error: string | null;
+ setProcessing: (isProcessing: boolean) => void;
+ setError: (error: string | null) => void;
+ generateContent: (params: {
+ prompt: string;
+ editorType: EditorType;
+ currentContent?: string;
+ }) => Promise;
+}
+
+const aiService = new AIService();
+
+export const useAIStore = create((set) => ({
+ isProcessing: false,
+ error: null,
+ setProcessing: (isProcessing) => set({ isProcessing }),
+ setError: (error) => set({ error }),
+ generateContent: async ({ prompt, editorType, currentContent }) => {
+ set({ isProcessing: true, error: null });
+ try {
+ const result = await aiService.getCompletion({
+ prompt,
+ editorType,
+ currentContent,
+ });
+ return result;
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
+ set({ error: errorMessage });
+ throw error;
+ } finally {
+ set({ isProcessing: false });
+ }
+ },
+}));