Browse Source

added functionality to gui to create your own patterns

pull/325/head
Jonathan Dunn 11 months ago
parent
commit
7eb5f953d7
  1. 45
      installer/client/gui/chatgpt.js
  2. 21
      installer/client/gui/index.html
  3. 174
      installer/client/gui/main.js
  4. 83
      installer/client/gui/static/js/index.js
  5. 24
      installer/client/gui/static/stylesheet/style.css

45
installer/client/gui/chatgpt.js

@ -1,45 +0,0 @@
const { OpenAI } = require("openai");
require("dotenv").config({
path: require("os").homedir() + "/.config/fabric/.env",
});
let openaiClient = null;
// Function to initialize and get the OpenAI client
function getOpenAIClient() {
if (!process.env.OPENAI_API_KEY) {
throw new Error(
"The OPENAI_API_KEY environment variable is missing or empty."
);
}
return new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
}
async function queryOpenAI(system, user, callback) {
const openai = getOpenAIClient(); // Ensure the client is initialized here
const messages = [
{ role: "system", content: system },
{ role: "user", content: user },
];
try {
const stream = await openai.chat.completions.create({
model: "gpt-4-1106-preview", // Adjust the model as necessary.
messages: messages,
temperature: 0.0,
top_p: 1,
frequency_penalty: 0.1,
presence_penalty: 0.1,
stream: true,
});
for await (const chunk of stream) {
const message = chunk.choices[0]?.delta?.content || "";
callback(message); // Process each chunk of data
}
} catch (error) {
console.error("Error querying OpenAI:", error);
callback("Error querying OpenAI. Please try again.");
}
}
module.exports = { queryOpenAI };

21
installer/client/gui/index.html

@ -36,6 +36,9 @@
>
Update Patterns
</button>
<button id="createPattern" class="btn btn-outline-success my-2 my-sm-0">
Create Pattern
</button>
<div class="collapse navbar-collapse" id="navbarCollapse"></div>
<div class="m1-auto">
<a class="navbar-brand" id="themeChanger" href="#">Dark</a>
@ -53,6 +56,24 @@
></textarea>
<button class="btn btn-primary" id="submit">Submit</button>
</div>
<div id="patternCreator" class="container hidden">
<input
type="text"
id="patternName"
placeholder="Enter Pattern Name"
class="form-control"
/>
<textarea
rows="5"
class="form-control"
id="patternBody"
placeholder="Create your pattern"
></textarea>
<button class="btn btn-primary" id="submitPattern">Submit</button>
<div id="patternCreatedMessage" class="hidden">
Pattern created successfully!
</div>
</div>
<div id="configSection" class="container hidden">
<input
type="text"

174
installer/client/gui/main.js

@ -1,10 +1,13 @@
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const fs = require("fs");
const fs = require("fs").promises;
const path = require("path");
const os = require("os");
const OpenAI = require("openai");
const Ollama = require("ollama");
const Anthropic = require("@anthropic-ai/sdk");
const axios = require("axios");
const fsExtra = require("fs-extra");
const fsConstants = require("fs").constants;
let fetch, allModels;
@ -17,26 +20,22 @@ let win;
let openai;
let ollama;
function ensureFabricFoldersExist() {
return new Promise(async (resolve, reject) => {
const fabricPath = path.join(os.homedir(), ".config", "fabric");
const patternsPath = path.join(fabricPath, "patterns");
async function ensureFabricFoldersExist() {
const fabricPath = path.join(os.homedir(), ".config", "fabric");
const patternsPath = path.join(fabricPath, "patterns");
try {
if (!fs.existsSync(fabricPath)) {
fs.mkdirSync(fabricPath, { recursive: true });
}
if (!fs.existsSync(patternsPath)) {
fs.mkdirSync(patternsPath, { recursive: true });
await downloadAndUpdatePatterns(patternsPath);
}
resolve(); // Resolve the promise once everything is set up
} catch (error) {
console.error("Error ensuring fabric folders exist:", error);
reject(error); // Reject the promise if an error occurs
}
});
try {
await fs
.access(fabricPath, fsConstants.F_OK)
.catch(() => fs.mkdir(fabricPath, { recursive: true }));
await fs
.access(patternsPath, fsConstants.F_OK)
.catch(() => fs.mkdir(patternsPath, { recursive: true }));
// Optionally download and update patterns after ensuring the directories exist
} catch (error) {
console.error("Error ensuring fabric folders exist:", error);
throw error; // Make sure to re-throw the error to handle it further up the call stack if necessary
}
}
async function downloadAndUpdatePatterns() {
@ -105,51 +104,77 @@ async function downloadAndUpdatePatterns() {
}
function getPatternFolders() {
const patternsPath = path.join(os.homedir(), ".config", "fabric", "patterns");
return fs
.readdirSync(patternsPath, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
return new Promise((resolve, reject) => {
fs.readdir(patternsPath, { withFileTypes: true }, (error, dirents) => {
if (error) {
console.error("Failed to read pattern folders:", error);
reject(error);
} else {
const folders = dirents
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
resolve(folders);
}
});
});
}
function checkApiKeyExists() {
async function checkApiKeyExists() {
const configPath = path.join(os.homedir(), ".config", "fabric", ".env");
return fs.existsSync(configPath);
try {
await fs.access(configPath, fsConstants.F_OK);
return true; // The file exists
} catch (e) {
return false; // The file does not exist
}
}
function loadApiKeys() {
async function loadApiKeys() {
const configPath = path.join(os.homedir(), ".config", "fabric", ".env");
let keys = { openAIKey: null, claudeKey: null };
if (fs.existsSync(configPath)) {
const envContents = fs.readFileSync(configPath, { encoding: "utf8" });
try {
// Use fs.promises.readFile and await its result
const envContents = await fs.readFile(configPath, { encoding: "utf8" });
const openAIMatch = envContents.match(/^OPENAI_API_KEY=(.*)$/m);
const claudeMatch = envContents.match(/^CLAUDE_API_KEY=(.*)$/m);
if (openAIMatch && openAIMatch[1]) {
keys.openAIKey = openAIMatch[1];
openai = new OpenAI({ apiKey: keys.openAIKey });
// Initialize your OpenAI client here if necessary
}
if (claudeMatch && claudeMatch[1]) {
keys.claudeKey = claudeMatch[1];
claude = new Anthropic({ apiKey: keys.claudeKey });
// Initialize your Claude client here if necessary
}
} catch (error) {
// Handle the case where the .env file doesn't exist or can't be read
console.error("Could not load API keys:", error);
}
return keys;
}
function saveApiKeys(openAIKey, claudeKey) {
async function saveApiKeys(openAIKey, claudeKey) {
const configPath = path.join(os.homedir(), ".config", "fabric");
const envFilePath = path.join(configPath, ".env");
if (!fs.existsSync(configPath)) {
fs.mkdirSync(configPath, { recursive: true });
try {
await fs.access(configPath);
} catch {
await fs.mkdir(configPath, { recursive: true });
}
let envContent = "";
// Read the existing .env file if it exists
if (fs.existsSync(envFilePath)) {
envContent = fs.readFileSync(envFilePath, "utf8");
try {
envContent = await fs.readFile(envFilePath, "utf8");
} catch (err) {
if (err.code !== "ENOENT") {
throw err;
}
// If the file doesn't exist, create an empty .env file
await fs.writeFile(envFilePath, "");
}
// Update the specific API key
@ -164,9 +189,9 @@ function saveApiKeys(openAIKey, claudeKey) {
claude = new Anthropic({ apiKey: claudeKey });
}
fs.writeFileSync(envFilePath, envContent.trim());
loadApiKeys();
win.webContents.send("reload-app");
await fs.writeFile(envFilePath, envContent.trim());
await loadApiKeys();
win.webContents.send("api-keys-saved");
}
function updateOrAddKey(envContent, keyName, keyValue) {
@ -195,7 +220,7 @@ async function getModels() {
ollamaModels: [],
};
let keys = loadApiKeys(); // Assuming loadApiKeys() is updated to return both keys
let keys = await loadApiKeys(); // Assuming loadApiKeys() is updated to return both keys
if (keys.claudeKey) {
// Assuming claudeModels do not require an asynchronous call to be fetched
@ -245,7 +270,7 @@ async function getModels() {
return allModels; // Return the aggregated results
}
function getPatternContent(patternName) {
async function getPatternContent(patternName) {
const patternPath = path.join(
os.homedir(),
".config",
@ -255,7 +280,8 @@ function getPatternContent(patternName) {
"system.md"
);
try {
return fs.readFileSync(patternPath, "utf8");
const content = await fs.readFile(patternPath, "utf8");
return content;
} catch (error) {
console.error("Error reading pattern file:", error);
return "";
@ -332,6 +358,33 @@ async function claudeMessage(system, user, model, event) {
event.reply("model-response-end", responseMessage);
}
async function createPatternFolder(patternName, patternBody) {
try {
const patternsPath = path.join(
os.homedir(),
".config",
"fabric",
"patterns"
);
const patternFolderPath = path.join(patternsPath, patternName);
// Create the pattern folder using the promise-based API
await fs.mkdir(patternFolderPath, { recursive: true });
// Create the system.md file inside the pattern folder
const filePath = path.join(patternFolderPath, "system.md");
await fs.writeFile(filePath, patternBody);
console.log(
`Pattern folder '${patternName}' created successfully with system.md inside.`
);
return `Pattern folder '${patternName}' created successfully with system.md inside.`;
} catch (err) {
console.error(`Failed to create the pattern folder: ${err.message}`);
throw err; // Ensure the error is thrown so it can be caught by the caller
}
}
function createWindow() {
win = new BrowserWindow({
width: 800,
@ -377,10 +430,21 @@ ipcMain.on("start-query", async (event, system, user, model) => {
}
});
ipcMain.handle("create-pattern", async (event, patternName, patternContent) => {
try {
const result = await createPatternFolder(patternName, patternContent);
return { status: "success", message: result }; // Use a response object for more detailed responses
} catch (error) {
console.error("Error creating pattern:", error);
return { status: "error", message: error.message }; // Return an error object
}
});
// Example of using ipcMain.handle for asynchronous operations
ipcMain.handle("get-patterns", async (event) => {
try {
return getPatternFolders();
const patterns = await getPatternFolders();
return patterns;
} catch (error) {
console.error("Failed to get patterns:", error);
return [];
@ -394,7 +458,8 @@ ipcMain.on("update-patterns", () => {
ipcMain.handle("get-pattern-content", async (event, patternName) => {
try {
return getPatternContent(patternName);
const content = await getPatternContent(patternName);
return content;
} catch (error) {
console.error("Failed to get pattern content:", error);
return "";
@ -403,7 +468,7 @@ ipcMain.handle("get-pattern-content", async (event, patternName) => {
ipcMain.handle("save-api-keys", async (event, { openAIKey, claudeKey }) => {
try {
saveApiKeys(openAIKey, claudeKey);
await saveApiKeys(openAIKey, claudeKey);
return "API Keys saved successfully.";
} catch (error) {
console.error("Error saving API keys:", error);
@ -423,22 +488,13 @@ ipcMain.handle("get-models", async (event) => {
app.whenReady().then(async () => {
try {
const keys = loadApiKeys();
if (!keys.openAIKey && !keys.claudeKey) {
promptUserForApiKey();
} else {
createWindow();
}
const keys = await loadApiKeys();
await ensureFabricFoldersExist(); // Ensure fabric folders exist
createWindow(); // Create the application window
// After window creation, check if the API key exists
if (!checkApiKeyExists()) {
console.log("API key is missing. Prompting user to input API key.");
win.webContents.send("request-api-key");
}
await getModels(); // Fetch models after loading API keys
createWindow(); // Keep this line
} catch (error) {
console.error("Failed to initialize fabric folders:", error);
await ensureFabricFoldersExist(); // Ensure fabric folders exist
createWindow(); // Keep this line
// Handle initialization failure (e.g., close the app or show an error message)
}
});

83
installer/client/gui/static/js/index.js

@ -11,6 +11,10 @@ document.addEventListener("DOMContentLoaded", async function () {
const openaiApiKeyInput = document.getElementById("apiKeyInput");
const claudeApiKeyInput = document.getElementById("claudeApiKeyInput");
const updatePatternsButton = document.getElementById("updatePatternsButton");
const updatePatternButton = document.getElementById("createPattern");
const patternCreator = document.getElementById("patternCreator");
const submitPatternButton = document.getElementById("submitPattern");
const myForm = document.getElementById("my-form");
const copyButton = document.createElement("button");
window.electronAPI.on("patterns-ready", () => {
@ -70,6 +74,35 @@ document.addEventListener("DOMContentLoaded", async function () {
);
}
async function submitPattern(patternName, patternText) {
try {
const response = await window.electronAPI.invoke(
"create-pattern",
patternName,
patternText
);
if (response.status === "success") {
console.log(response.message);
// Show success message
const patternCreatedMessage = document.getElementById(
"patternCreatedMessage"
);
patternCreatedMessage.classList.remove("hidden");
setTimeout(() => {
patternCreatedMessage.classList.add("hidden");
}, 3000); // Hide the message after 3 seconds
// Update pattern list
loadPatterns();
} else {
console.error(response.message);
// Handle failure (e.g., showing an error message to the user)
}
} catch (error) {
console.error("IPC error:", error);
}
}
function copyToClipboard() {
const containerClone = responseContainer.cloneNode(true);
const copyButtonClone = containerClone.querySelector("#copyButton");
@ -135,13 +168,12 @@ document.addEventListener("DOMContentLoaded", async function () {
});
} catch (error) {
console.error("Failed to load models:", error);
alert(
"Failed to load models. Please check the console for more details."
);
}
}
updatePatternsButton.addEventListener("click", () => {
window.electronAPI.send("update-patterns");
});
// Load patterns and models on startup
loadPatterns();
loadModels();
@ -168,13 +200,18 @@ document.addEventListener("DOMContentLoaded", async function () {
submitQuery(message);
});
window.electronAPI.on;
"reload-app",
() => {
// Reload the app
loadModels();
location.reload();
};
window.electronAPI.on("api-keys-saved", async () => {
try {
await loadModels();
alert("API Keys saved successfully.");
configSection.classList.add("hidden");
openaiApiKeyInput.value = "";
claudeApiKeyInput.value = "";
} catch (error) {
console.error("Failed to reload models:", error);
alert("Failed to reload models.");
}
});
// Submit button click handler
submitButton.addEventListener("click", async () => {
@ -182,6 +219,14 @@ document.addEventListener("DOMContentLoaded", async function () {
submitQuery(userInputValue);
});
submitPatternButton.addEventListener("click", async () => {
const patternName = document.getElementById("patternName").value;
const patternText = document.getElementById("patternBody").value;
document.getElementById("patternName").value = "";
document.getElementById("patternBody").value = "";
submitPattern(patternName, patternText);
});
// Theme changer click handler
themeChanger.addEventListener("click", function (e) {
e.preventDefault();
@ -190,6 +235,14 @@ document.addEventListener("DOMContentLoaded", async function () {
themeChanger.innerText === "Dark" ? "Light" : "Dark";
});
updatePatternButton.addEventListener("click", function (e) {
e.preventDefault();
patternCreator.classList.toggle("hidden");
myForm.classList.toggle("hidden");
// window.electronAPI.send("create-pattern");
});
// Config button click handler - toggles the config section visibility
configButton.addEventListener("click", function (e) {
e.preventDefault();
@ -202,14 +255,6 @@ document.addEventListener("DOMContentLoaded", async function () {
const claudeKey = claudeApiKeyInput.value;
window.electronAPI
.invoke("save-api-keys", { openAIKey, claudeKey })
.then(() => {
alert("API Keys saved successfully.");
configSection.classList.add("hidden");
openaiApiKeyInput.value = "";
claudeApiKeyInput.value = "";
// Reload the models
loadModels();
})
.catch((err) => {
console.error("Error saving API keys:", err);
alert("Failed to save API Keys.");

24
installer/client/gui/static/stylesheet/style.css

@ -158,3 +158,27 @@ body.light-theme .navbar-toggler-icon {
#copyButton:focus {
outline: none;
}
#patternCreatedMessage {
margin-top: 10px;
padding: 10px;
background-color: #4caf50;
color: white;
border-radius: 5px;
}
.light-theme #patternCreator {
background: #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.light-theme #patternCreator input,
.light-theme #patternCreator textarea {
background-color: #fff;
color: #333;
border: 1px solid #ddd;
}
#patternCreator textarea {
background-color: #424242;
color: #e0e0e0;
border: 1px solid #555;
}

Loading…
Cancel
Save