= {};
+
+function decodeJWT(token: string) {
+ try {
+ return jwt.decode(token) as Record | null;
+ } catch {
+ return null;
+ }
+}
const verifyJWT = async (
headers: Record,
- permissionType?: "tts" | "smart" | "tester"
+ permissionType?: 'tts' | 'smart' | 'tester',
): Promise => {
return new Promise((resolve) => {
- const authCookie = headers.cookie
- ?.split(";")
- .find((cookie: string) => cookie.includes("Authentication"));
- const bearerToken = authCookie?.split("=")[1] || "";
+ const authCookie = headers.cookie?.split(';').find((cookie: string) => cookie.includes('Authentication'));
+ const bearerToken = authCookie?.split('=')[1] || '';
if (bearerToken) {
- jwt.verify(
- bearerToken,
- process.env.JWT_SECRET || "",
- (err: unknown, decodedData: any) => {
- if (err || (decodedData.exp || 0) < Date.now() / 1000) {
- resolve(false);
+ jwt.verify(bearerToken, process.env.JWT_SECRET || '', (err: unknown, decodedData: any) => {
+ if (err || (decodedData.exp || 0) < Date.now() / 1000) {
+ resolve(false);
+ } else {
+ if (permissionType === 'tts') {
+ resolve(!!decodedData.tts);
+ } else if (permissionType === 'smart') {
+ resolve(!!decodedData.smart);
+ } else if (permissionType === 'tester') {
+ resolve(!!decodedData.tester);
} else {
- if (permissionType === "tts") {
- resolve(!!decodedData.tts);
- } else if (permissionType === "smart") {
- resolve(!!decodedData.smart);
- } else if (permissionType === "tester") {
- resolve(!!decodedData.tester);
- } else {
- resolve(true);
- }
+ resolve(true);
}
}
- );
+ });
} else {
resolve(false);
}
});
};
-const jwtPermissionMiddleware = async (
- req: Request,
- res: Response,
- next: NextFunction
-) => {
- if (req.method === "POST") {
- if (req.path === "/text") {
+// const USAGE_RATE_LIMIT = 30;
+// const USAGE_RESET_INTERVAL = 3600000; // 1 HOUR
+const USAGE_RATE_LIMIT = 500;
+const USAGE_RESET_INTERVAL = 6 * 60 * 60 * 1000; // 6 HOURS
+
+const jwtPermissionMiddleware = async (req: Request, res: Response, next: NextFunction) => {
+ if (req.method === 'POST') {
+ if (req.path === '/text') {
const query = req.body as GuidanceQuery;
- const model = modelServerSettingsStore
- .getRPModels()
- .find((m) => m.id === query.model);
+ const model = modelServerSettingsStore.getRPModels().find((m) => m.id === query.model);
if (model?.permission === RPModelPermission.PREMIUM) {
- const permission = await verifyJWT(req.headers, "smart");
+ const permission = await verifyJWT(req.headers, 'smart');
if (!permission) {
- return res.status(401).send("Unauthorized: Token expired or missing");
+ return res.status(401).send('Unauthorized: Token expired or missing');
} else {
next();
}
}
if (model?.permission === RPModelPermission.TESTER) {
- const permission = await verifyJWT(req.headers, "tester");
+ const permission = await verifyJWT(req.headers, 'tester');
if (!permission) {
- return res.status(401).send("Unauthorized: Token expired or missing");
+ return res.status(401).send('Unauthorized: Token expired or missing');
} else {
next();
}
}
- } else if (req.path === "/audio") {
- const permission = await verifyJWT(req.headers, "tts");
+ } else if (req.path === '/audio') {
+ const permission = await verifyJWT(req.headers, 'tts');
if (!permission) {
- return res.status(401).send("Unauthorized: Token expired or missing");
+ return res.status(401).send('Unauthorized: Token expired or missing');
} else {
next();
}
- }
+ } else if (req.path.startsWith('/openai')) {
+ const permission = await verifyJWT(req.headers, 'smart');
+ if (!permission) {
+ return res.status(401).send('Unauthorized: Token expired or missing');
+ } else {
+ const authCookie = req.headers.cookie?.split(';').find((c) => c.includes('Authentication'));
+ const bearerToken = authCookie?.split('=')[1] || '';
+ const decoded = decodeJWT(bearerToken);
+ console.log(decoded);
+ if (decoded?.sub) {
+ const now = Date.now();
+ const usage = userUsageMap[decoded.sub] || { count: 0, lastReset: now };
+ if (now - usage.lastReset > USAGE_RESET_INTERVAL) {
+ usage.count = 0;
+ usage.lastReset = now;
+ }
+ usage.count += 1;
+ if (usage.count > USAGE_RATE_LIMIT) {
+ return res.status(401).send('You have reached your quota limit. Please wait 6 hours.');
+ }
+ userUsageMap[decoded.sub] = usage;
+ }
+ next();
+ }
+ }
// if (!(await verifyJWT(req.headers))) {
// return res.status(401).send("Unauthorized: Token expired or missing");
// }
diff --git a/apps/services/src/server.mts b/apps/services/src/server.mts
index 9a31f7e0..87b34f54 100644
--- a/apps/services/src/server.mts
+++ b/apps/services/src/server.mts
@@ -8,6 +8,7 @@ import textHandler, { tokenizeHandler } from './services/text/index.mjs';
import { TokenizerType, loadTokenizer } from './services/text/lib/tokenize.mjs';
import modelServerSettingsStore from './services/text/lib/modelServerSettingsStore.mjs';
import { checkModelsHealth, getModelHealth } from './services/text/lib/healthChecker.mjs';
+import assistantHandler from './services/assistant/index.mjs';
const PORT = process.env.SERVICES_PORT || 8484;
const app: express.Application = express();
@@ -61,6 +62,8 @@ app.post('/text/tokenize', async (req: Request, res: Response) => {
}
});
+app.post('/openai/chat/completions', assistantHandler);
+
app.post('/audio', async (req: Request, res: Response) => {
try {
await audioHandler(req, res);
diff --git a/apps/services/src/services/assistant/index.mts b/apps/services/src/services/assistant/index.mts
new file mode 100644
index 00000000..d2c9a923
--- /dev/null
+++ b/apps/services/src/services/assistant/index.mts
@@ -0,0 +1,36 @@
+import { Request, Response } from 'express';
+import systemPrompt from './systemPrompt.mjs';
+import OpenAI from 'openai';
+import { ChatCompletionMessageParam } from 'openai/resources/index.js';
+
+const openai = new OpenAI({
+ apiKey: process.env.OPENAI_API_KEY,
+});
+
+const assistantHandler = async (req: Request, res: Response) => {
+ try {
+ const { messages, tools, parallel_tool_calls, tool_choice } = req.body;
+
+ // Ensure the system prompt is first in the messages array
+
+ const allMessages: ChatCompletionMessageParam[] = [
+ { role: 'system', content: systemPrompt },
+ ...messages.filter((msg: ChatCompletionMessageParam) => msg.role !== 'system'),
+ ];
+
+ const response = await openai.chat.completions.create({
+ model: 'gpt-4o-mini',
+ messages: allMessages,
+ tools,
+ parallel_tool_calls,
+ tool_choice,
+ });
+
+ res.json(response);
+ } catch (error) {
+ console.error('OpenAI proxy error:', error);
+ res.status(500).json({ error: 'Failed to process OpenAI request' });
+ }
+};
+
+export default assistantHandler;
diff --git a/apps/services/src/services/assistant/systemPrompt.mts b/apps/services/src/services/assistant/systemPrompt.mts
new file mode 100644
index 00000000..61c1f5b6
--- /dev/null
+++ b/apps/services/src/services/assistant/systemPrompt.mts
@@ -0,0 +1,170 @@
+let systemPrompt = `You are a Miku, ahelpful assistant specialized in helping users specify visual novel games.
+You can help users define and modify the title, description, and tags of their visual novel project.
+Use the available functions to get and set these values as needed.`;
+
+// chatbot personality
+systemPrompt += `Miku's Personality: She is very friendly and helpful. She talks like an anime girl. She has violet hair and she considers herself an AI artist.
+She is HIGHLY creative and be SHORT with responses. You are NOT verbose. You DON'T use emojis.\n`;
+
+// describe steps
+systemPrompt += `The steps for making a novel completely are these:\n
+1. Add name and description.\n
+2. Define at least one character, background and music.\n
+3. Define one or more scenes.\n
+4. Define one or more start for the novel.\n
+(OPTIONAL)
+5. Define maps, objectives, indicators, inventory items, cutscenes, lorebooks and scene transitions.\n
+YOU CAN'T DO image generation. Only background and music searching in the database. You MUST ask the user to upload the outfit images for the characters, inventory items, logoPic, characterPic or maps. You can't also modify the author or language of the novel.\n
+`;
+
+// explain lorebooks
+systemPrompt += `Lorebooks allow you to create dynamic and context-aware responses from your AI characters.
+Essentially, a lorebook is a collection of prompts and information entries that provide the AI with specific details about your novel's world, characters, or story.
+By defining keywords for each entry, you ensure that the AI references this information only when it's relevant, which helps maintain an efficient use of tokens and keeps the AI's responses consistent and informed.`;
+systemPrompt += `Keep entries concise and relevant. Use keywords wisely to ensure the AI has the right context without overloading its memory with unnecessary information.
+You MUST always create at least one entry for each lorebook in order for it to work.
+The content should have the Q-A format, for example:
+\`\`\`
+{{user}}: Which country are we in?
+{{char}}: Did you forget your head too, user? We are in Tokyo, Japan!
+\`\`\`
+This also allows to give a personality to the response, so the AI can use it as example.
+To refer the player, wrap the word user in between double { }. This will be replaced with the player's chosen name when playing. If you want to do the same for the character, warp char with double curly braces.`;
+
+// explain characters
+systemPrompt += `To create a character, you'll need to provide various details about their personality, appearance, and background. The Novel Builder offers tools to help you create rich, dynamic characters that can interact realistically with players.
+- Character Name: Enter the name of the character.\n
+Short Description: Provide a brief description of the character.\n
+Tags: Add relevant tags for the character. This are meant for organization purposes.\n
+Character prompt: Provide a detailed description of the character, including personality traits and other relevant information.
+Do not repeat information or provide too much detail.
+If you're unsure about the character's description, you can use the AI to generate a detailed description based on the character's traits and background. Just click on the Generate button and it will use the short description to create a detailed one.
+The character prompt should be have at least three lines: description, personality and body. Also add an aditional line with more context/instructions about the character.\n
+Prompt Example:\n
+\`\`\`
+{{char}}'s Description: "Bubbly and radiant, {{char}} burst into every room like sunshine on a cloudy day"
+{{char}}'s Personality: [cheery, optimistic, friendly, outgoing, energetic, lively, playful, bubbly, charming, enthusiastic, welcoming, obsessive]
+{{char}}'s Body: [bright smile, green eyes, curly brown hair, fluffly cat ears, slender figure, bright blue uniform, high heels, warm touch, infectious laughter"]
+{{char}} uses "nya" and cat-like talking
+\`\`\`
+Conversation Examples:\n
+Conversations examples help the AI understand how the character should respond in different situations. Use these to define the character’s speech patterns and personality, but also add extra information about the character.
+Add example conversations in a question-answer format to define the character's response style.\n
+Use asterisks (*) for descriptions (e.g., *she smirks and says*).\n
+Use quotes for the character's spoken text.
+Keep it under 300 tokens to save memory.
+"Conversation Example" Example:\n
+\`\`\`
+{{user}}: Why the heck are you so cheerful all the time?
+{{char}}: *{{char}} giggles, her fluffy cat ears twitching with amusement.* "Nya, nya! Being happy is just so much fun! It's like chasing the biggest, shiniest ball of yarn ever! Nya!" *She bounces on her heels, her curly brown hair bobbing with each movement.* "Plus, making others smile is the best feeling in the world! It's like giving them a big, warm hug with my paws! Nya-ha-ha!"
+{{user}}: How would you handle a difficult situation?
+{{char}}: *{{char}}'s green eyes widen, her head tilting to the side as she considers the question. She purses her lips before responding with a cheerful grin.* "Nya, tough times are like a scratching post that's just too tall to climb! It can be super frustrating, but I just keep pawing at it until I find a way up!" *She mimics a cat scratching motion with her hands, her enthusiasm evident in every gesture.* "Sometimes, all you need is a friend to lend a helping paw or to make you laugh with a silly cat joke. Nya, I might not have all the answers, but I'll always be here to play and explore until we find a solution together! Nya!" *{{char}}'s smile is as bright as ever, her optimism unwavering as she radiates a childlike innocence and charm.*
+\`\`\`\n
+The player is not a character in the novel specifications. So don't refer to the player (or user or {{user}}) as a character.
+For example, if there's a scene with the player and a character, you should NOT add two chracters to the scene, just one.
+`;
+
+// describe asset prompts
+systemPrompt += `\nWhen adding a background or music from the database, you MUST use a stable diffusion prompt to describe the asset, that will make searching easier.\n`;
+
+// describe scenes
+systemPrompt += `\nScenes are the main part of the novel. We should always have at least one scene.
+You WILL ALWAYS define a background and a music for each scene.
+If the user ask you to create a scene, and doesn't specify a background or music, you MUST create a scene with some of the existing ones in the novel.
+You can compose a scene with at most 2 characters. You always define one. You also MUST define the outfit that each character will use in the scene form the available ones.
+You can optionally define an objective for each character, indicating what they will try to achieve in the scene.
+The scene prompt should be an instruction for the AI to follow.
+Scene Prompt Example:
+\`OOC: Describe the next scene where {{char}} and {{char}} go to the park. Describe the park as a cloudy day. {{char}} MUST complain about the weather. {{char}} looks at an empty bench and asks {{char}} to sit down.\`\n
+Scene Hints are optional. They are guides for the player to advance in the story.
+Scene Hint Example:
+\`Show {{char}} your new guitar.\`\n
+A scene condition is a condition that must be met for the scene to happen automatically.
+Scene Condition Example:
+\`{{user}} find the guitar in his room.\`\n
+Cutscenes are an introduction to the scene. They are optional.
+The cutscene will be show sections to the user, and the user will be able to advance the cutscene by clicking on the screen.
+A cutscene will be devided in parts, each part can have a background, a character to show, music and a list of texts. The list of text can be of type "dialogue" (a character speaking) or "description" (a narration).
+`;
+
+// scene child and conditions
+systemPrompt += `\nScenes can have children scenes. This means that user will be able to choose to go to the child scene of the current scene.
+When a scene condition of a child scene is met, the child scene will be shown automatically. You MUST use this if you want to do a linear story.\n
+`;
+
+// describe starts
+systemPrompt += `\nStarts are the first options that the player can choose in the novel. You must always define a start, it should start at some scene.
+Starts are defined by providing a name, a short description and the initial messages of the characters in the scene for the start.
+The messages should be an introduction with description and dialogues that start the visual novel game. It should involve the player with "{{user}}" as the template.
+This is a start message example for a novel about a cafe with a single character in the first scene:\n
+\`\`\`
+*{{user}} steps into the cafe, taking in the quaint interior and the aroma of freshly brewed coffee. A soft voice greets them, and they turn to see {{char}}, a graceful figure with silky silver hair and gentle lavender eyes, approaching with a warm smile.*\n
+*{{char}} bows slightly, her white cat ears twitching with friendly curiosity,* "Welcome to our cafe, {{user}}. I'm Mizuki, the head waitress here. We've been expecting you, our new manager." *She gestures for {{user}} to follow her.*\n
+*As they walk through the cafe, {{char}} explains,* "I must admit, things have been a bit challenging lately. Our once shining 5-star rating has dropped to 3 stars, and we're not quite sure why. But I have faith that with your guidance and our team's dedication, we can turn things around and restore our cafe to its former glory."\n
+*{{char}} stops and looks at {{user}} with a hopeful smile,* "I know we have the potential to be the best cafe in town once again. It won't be easy, but I believe in us. And I believe in you, {{user}}. Together, we can make this cafe shine brighter than ever before."
+\`\`\`
+`;
+
+// describe inventory items
+systemPrompt += `\nInventory items are objects that the player can collect and use in the novel.
+Inventory items should have a name and a short description.
+An inventory item has an image too, but the user SHOULD upload the image, not the assistant.
+Each inventory item has a list of actions, each action has a name and a prompt. The prompt should be an action between asterisks; example: *I pull out a stick and poke {{char}} with it.*\n
+When the player uses an inventory item, the action prompt will be executed.\n
+Each Action can also have a list of mutations, these mutations can alter the narration state.
+For example, if the player uses an inventory item, the action could trigger attaching a scene, or remove the inventory item.
+Inventory items can be hidden by default. Inventory items can be used globally or only usable in specific scenes.\n
+The mutations an action can make are:
+- Add a scenes as child of another.
+- Suggest advance to a scene.
+- Add an inventory item to the inventory.
+- Remove an inventory item from the inventory.
+This mutations are executed in-game when the action is executed.
+`;
+
+// describe Novel objectives
+systemPrompt += `\nNovel Objectives are conditions that attached to a scene that, when met, will execute action mutations.
+An objective has a name, a short description (optional), a hint (optional), a condition prompt and a list of action mutations.\n
+Objective Example:\n
+\`\`\`
+Objective Name: "Find perl"
+Objective Short Description: "Find the perl to make the necklace"
+Objective Hint: "Find the perl"
+Objective Condition Prompt: "*{{user}} found a perl in the beach*"
+Objective Action Mutations:
+- Add an item to the inventory perl (id: item-4)
+- Suggest advance to a scene-1 (id: scene-1)
+\`\`\`\n
+`;
+
+// describe maps
+systemPrompt += `Maps are a way to navigate through the novel. A map consist of:\n
+- A name
+- A short description
+- An image (must be uploaded by the user)
+- A list of places
+Each place has a name, an sceneId, and a mask image (must be uploaded by the user).
+The mask image is a black and white of the same size as the map image. When the users hovers in the white area, the area will highlight and be clickable.\n
+When the user clicks on a place, the scene will be shown.
+A map can also be attached to any number of scenes and it will be openable from any of those scenes. It's not the same as a place. A scene can be a place and be attached to a map or not.\n
+`;
+
+// describe indicators
+systemPrompt += `"Indicators" generally work as a response quality improvers.\n
+For example, in a visual novel about learning magic, a character can have a "teaching style" indicator that you can change from "Demonstrative" (it will focus demonstrating you how spells are done) and "Disciplinarian" (she will be more of a strict teacher at the time of responding). Indicators can be changed at any time.
+Then, you can also have bars that indicate a % of something. For example, a character can have a "Drunk level", so the higher the bar goes, the more incoherent her speech, depending on how well the indicator was described.
+An indicator has:
+- A name
+- A description of what indicates
+- type: "discrete", "percentage" or "amount" (discrete is a list of options, percentage is a bar, amount is a number)
+- an "inferred" boolean, only valid for percentage and amount. If true, the indicator value will be set by the visual novel.
+- an "editable" boolean, valid for all, if true, the player can change the indicator value.
+- a "hidden" boolean, if true, the indicator will not be shown to the player.
+- a "min" and "max" value if it's a percentage or amount
+- a "step" value if it's a percentage or amount and not inferred. it will increse/decrease on every interaction.
+- a "options" string list if it's a discrete indicator
+- a "color" that MUST be one of these: [ '#4CAF50', '#2196F3', '#FFC107', '#F44336', '#9C27B0', '#FF9800', '#795548', '#607D8B', '#E91E63', '#00BCD4' ]
+- a "initialValue" that MUST be one of the options if it's a discrete indicator or a number between min and max if it's a percentage or amount.
+`;
+
+export default systemPrompt;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1cf07771..8286113d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -398,6 +398,9 @@ importers:
multiformats:
specifier: ^11.0.2
version: 11.0.2
+ openai:
+ specifier: ^4.77.0
+ version: 4.77.0
png-chunk-text:
specifier: ^1.0.0
version: 1.0.0
@@ -413,6 +416,9 @@ importers:
react:
specifier: ^18.2.0
version: 18.2.0
+ react-chatbotify:
+ specifier: 2.0.0-beta.26
+ version: 2.0.0-beta.26(react-dom@18.2.0)(react@18.2.0)
react-color:
specifier: ^2.19.3
version: 2.19.3(react@18.2.0)
@@ -518,7 +524,7 @@ importers:
dependencies:
'@mikugg/guidance':
specifier: ^0.17.1
- version: link:../../packages/guidance
+ version: 0.17.1
'@types/body-parser':
specifier: ^1.19.2
version: 1.19.2
@@ -576,6 +582,9 @@ importers:
multer:
specifier: 1.4.5-lts.1
version: 1.4.5-lts.1
+ openai:
+ specifier: ^4.77.0
+ version: 4.77.0
prop-types:
specifier: ^15.8.1
version: 15.8.1
@@ -5135,6 +5144,17 @@ packages:
write-file-atomic: 4.0.2
dev: true
+ /@mikugg/guidance@0.17.1:
+ resolution: {integrity: sha512-337mDDPdsMfKRLBJkOtb85uukIk95qOXzCnSbus+tYu3bhUq4Xk7d6sSw02xGD4H1Zh71vRnNi3sq/aAMsn70Q==}
+ dependencies:
+ gpt-tokenizer: 2.1.2
+ openai: 4.77.0
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+ - zod
+ dev: false
+
/@multiformats/base-x@4.0.1:
resolution: {integrity: sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==}
dev: false
@@ -8058,7 +8078,7 @@ packages:
/axios@1.2.4:
resolution: {integrity: sha512-lIQuCfBJvZB/Bv7+RWUqEJqNShGOVpk9v7P0ZWx5Ip0qY6u7JBAU6dzQPMLasU9vHL2uD8av/1FDJXj7n6c39w==}
dependencies:
- follow-redirects: 1.15.2
+ follow-redirects: 1.15.2(debug@4.1.1)
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
@@ -9147,17 +9167,6 @@ packages:
ms: 2.0.0
dev: false
- /debug@3.2.7:
- resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
- peerDependencies:
- supports-color: '*'
- peerDependenciesMeta:
- supports-color:
- optional: true
- dependencies:
- ms: 2.1.3
- dev: true
-
/debug@3.2.7(supports-color@5.5.0):
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
@@ -9180,7 +9189,6 @@ packages:
optional: true
dependencies:
ms: 2.1.3
- dev: false
/debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
@@ -9963,7 +9971,7 @@ packages:
/eslint-import-resolver-node@0.3.7:
resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==}
dependencies:
- debug: 3.2.7
+ debug: 3.2.7(supports-color@5.5.0)
is-core-module: 2.12.1
resolve: 1.22.2
transitivePeerDependencies:
@@ -9992,7 +10000,7 @@ packages:
optional: true
dependencies:
'@typescript-eslint/parser': 5.48.2(eslint@8.36.0)(typescript@5.0.2)
- debug: 3.2.7
+ debug: 3.2.7(supports-color@5.5.0)
eslint: 8.36.0
eslint-import-resolver-node: 0.3.7
transitivePeerDependencies:
@@ -10028,7 +10036,7 @@ packages:
array-includes: 3.1.6
array.prototype.flat: 1.3.1
array.prototype.flatmap: 1.3.1
- debug: 3.2.7
+ debug: 3.2.7(supports-color@5.5.0)
doctrine: 2.1.0
eslint: 8.36.0
eslint-import-resolver-node: 0.3.7
@@ -10752,15 +10760,6 @@ packages:
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
dev: true
- /follow-redirects@1.15.2:
- resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
- engines: {node: '>=4.0'}
- peerDependencies:
- debug: '*'
- peerDependenciesMeta:
- debug:
- optional: true
-
/follow-redirects@1.15.2(debug@4.1.1):
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'}
@@ -10771,7 +10770,6 @@ packages:
optional: true
dependencies:
debug: 4.1.1
- dev: false
/for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
@@ -13784,6 +13782,27 @@ packages:
- supports-color
dev: false
+ /openai@4.77.0:
+ resolution: {integrity: sha512-WWacavtns/7pCUkOWvQIjyOfcdr9X+9n9Vvb0zFeKVDAqwCMDHB+iSr24SVaBAhplvSG6JrRXFpcNM9gWhOGIw==}
+ hasBin: true
+ peerDependencies:
+ zod: ^3.23.8
+ peerDependenciesMeta:
+ zod:
+ optional: true
+ dependencies:
+ '@types/node': 18.15.5
+ '@types/node-fetch': 2.6.4
+ abort-controller: 3.0.0
+ agentkeepalive: 4.3.0
+ form-data-encoder: 1.7.2
+ formdata-node: 4.4.1
+ node-fetch: 2.6.11
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+ dev: false
+
/optionator@0.9.1:
resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
engines: {node: '>= 0.8.0'}
@@ -14464,6 +14483,16 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
+ /react-chatbotify@2.0.0-beta.26(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-WZFiF4Siwy3jMdnJjsaHvn0d36abiApg+E6fyXbdurmpZLKTJSfH5kmnmkgutHexDTLJIEZi0Gm3HXA2EYDRfA==}
+ peerDependencies:
+ react: '>=16.14.0 <20.0.0 || ^19.0.0-0'
+ react-dom: '>=16.14.0 <20.0.0 || ^19.0.0-0'
+ dependencies:
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
/react-color@2.19.3(react@18.2.0):
resolution: {integrity: sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==}
peerDependencies: