mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-20 11:22:55 -05:00
dashboard apis
This commit is contained in:
51
apps/web/app/api/analytics/_lib/cube-client.ts
Normal file
51
apps/web/app/api/analytics/_lib/cube-client.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import cubejs from "@cubejs-client/core";
|
||||
|
||||
/**
|
||||
* Cube.js client for executing queries
|
||||
* No authentication - for POC purposes only
|
||||
*/
|
||||
|
||||
// Cube API configuration - defaults to localhost:4000, can be overridden via env var
|
||||
// Automatically append /cubejs-api/v1 if not present
|
||||
const getApiUrl = () => {
|
||||
const baseUrl = process.env.CUBEJS_API_URL || "http://localhost:4000";
|
||||
// If the URL already contains /cubejs-api/v1, use it as-is
|
||||
if (baseUrl.includes("/cubejs-api/v1")) {
|
||||
return baseUrl;
|
||||
}
|
||||
// Otherwise, append the path
|
||||
return `${baseUrl.replace(/\/$/, "")}/cubejs-api/v1`;
|
||||
};
|
||||
|
||||
const API_URL = getApiUrl();
|
||||
|
||||
/**
|
||||
* Create a Cube.js client instance without authentication
|
||||
* For POC - Cube.js must be configured to allow unauthenticated requests
|
||||
*/
|
||||
export function createCubeClient() {
|
||||
// Empty string = no authentication token
|
||||
return cubejs("", {
|
||||
apiUrl: API_URL,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a Cube.js query and return the table pivot data
|
||||
* @param query - The Cube.js query object
|
||||
* @returns Array of row objects with measure/dimension values
|
||||
*/
|
||||
export async function executeQuery(query: any) {
|
||||
try {
|
||||
const client = createCubeClient();
|
||||
console.log("Executing Cube.js query:", JSON.stringify(query, null, 2));
|
||||
console.log("Cube.js API URL:", API_URL);
|
||||
const resultSet = await client.load(query);
|
||||
return resultSet.tablePivot();
|
||||
} catch (error: any) {
|
||||
console.error("Cube.js query error:", error);
|
||||
console.error("Query that failed:", JSON.stringify(query, null, 2));
|
||||
console.error("API URL used:", API_URL);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
286
apps/web/app/api/analytics/_lib/schema-parser.ts
Normal file
286
apps/web/app/api/analytics/_lib/schema-parser.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* Parses the Cube.js schema file to extract measures and dimensions
|
||||
* This keeps the schema as the single source of truth for AI query generation
|
||||
*/
|
||||
|
||||
interface MeasureInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface DimensionInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
type: "string" | "number" | "time";
|
||||
}
|
||||
|
||||
// Path to schema file - self-contained within the analytics folder
|
||||
const SCHEMA_FILE_PATH = path.join(process.cwd(), "app", "api", "analytics", "_schema", "FeedbackRecords.js");
|
||||
|
||||
/**
|
||||
* Extract description from a schema property object
|
||||
*/
|
||||
function extractDescription(objStr: string): string {
|
||||
const descMatch = objStr.match(/description:\s*`([^`]+)`/);
|
||||
return descMatch ? descMatch[1] : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract type from a dimension object
|
||||
*/
|
||||
function extractType(objStr: string): "string" | "number" | "time" {
|
||||
const typeMatch = objStr.match(/type:\s*`(string|number|time)`/);
|
||||
return (typeMatch?.[1] as "string" | "number" | "time") || "string";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to extract content inside the first matching brace block
|
||||
*/
|
||||
function extractInnerBlockContent(content: string, startRegex: RegExp): string | null {
|
||||
const match = content.match(startRegex);
|
||||
if (!match) return null;
|
||||
|
||||
// Backtrack to find the opening brace in the match
|
||||
const braceIndex = match[0].lastIndexOf("{");
|
||||
if (braceIndex === -1) return null; // Should not happen given regex usage
|
||||
|
||||
// Actually we can just start scanning from the end of the match if the regex ends with {
|
||||
// But let's be safer: start counting from the opening brace.
|
||||
const absoluteStartIndex = match.index! + braceIndex;
|
||||
|
||||
let braceCount = 1;
|
||||
let i = absoluteStartIndex + 1;
|
||||
|
||||
while (braceCount > 0 && i < content.length) {
|
||||
if (content[i] === "{") braceCount++;
|
||||
else if (content[i] === "}") braceCount--;
|
||||
i++;
|
||||
}
|
||||
|
||||
if (braceCount === 0) {
|
||||
return content.substring(absoluteStartIndex + 1, i - 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse measures from the schema file
|
||||
*/
|
||||
function parseMeasures(schemaContent: string): MeasureInfo[] {
|
||||
const measures: MeasureInfo[] = [];
|
||||
|
||||
const measuresBlock = extractInnerBlockContent(schemaContent, /measures:\s*\{/);
|
||||
if (!measuresBlock) return measures;
|
||||
|
||||
// Match each measure: measureName: { ... }
|
||||
const measureRegex = /(\w+):\s*\{/g;
|
||||
let match;
|
||||
|
||||
while ((match = measureRegex.exec(measuresBlock)) !== null) {
|
||||
const name = match[1];
|
||||
const startIndex = match.index + match[0].length;
|
||||
|
||||
// Find the matching closing brace
|
||||
let braceCount = 1;
|
||||
let endIndex = startIndex;
|
||||
|
||||
while (braceCount > 0 && endIndex < measuresBlock.length) {
|
||||
if (measuresBlock[endIndex] === "{") braceCount++;
|
||||
if (measuresBlock[endIndex] === "}") braceCount--;
|
||||
endIndex++;
|
||||
}
|
||||
|
||||
const body = measuresBlock.substring(startIndex, endIndex - 1);
|
||||
const description = extractDescription(body);
|
||||
|
||||
if (description) {
|
||||
measures.push({ name, description });
|
||||
}
|
||||
}
|
||||
|
||||
return measures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse dimensions from a specific cube
|
||||
*/
|
||||
function parseDimensionsFromCube(cubeContent: string, cubeName: string): DimensionInfo[] {
|
||||
const dimensions: DimensionInfo[] = [];
|
||||
|
||||
const dimensionsBlock = extractInnerBlockContent(cubeContent, /dimensions:\s*\{/);
|
||||
if (!dimensionsBlock) return dimensions;
|
||||
|
||||
// Match each dimension: dimensionName: { ... }
|
||||
const dimensionRegex = /(\w+):\s*\{/g;
|
||||
let match;
|
||||
|
||||
while ((match = dimensionRegex.exec(dimensionsBlock)) !== null) {
|
||||
const name = match[1];
|
||||
const startIndex = match.index + match[0].length;
|
||||
|
||||
// Find the matching closing brace
|
||||
let braceCount = 1;
|
||||
let endIndex = startIndex;
|
||||
|
||||
while (braceCount > 0 && endIndex < dimensionsBlock.length) {
|
||||
if (dimensionsBlock[endIndex] === "{") braceCount++;
|
||||
if (dimensionsBlock[endIndex] === "}") braceCount--;
|
||||
endIndex++;
|
||||
}
|
||||
|
||||
const body = dimensionsBlock.substring(startIndex, endIndex - 1);
|
||||
const description = extractDescription(body);
|
||||
const type = extractType(body);
|
||||
|
||||
// Skip primaryKey dimensions (like 'id') and internal dimensions
|
||||
if (body.includes("primaryKey: true") || name === "feedbackRecordId") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (description) {
|
||||
dimensions.push({
|
||||
name: cubeName === "FeedbackRecords" ? name : `${cubeName}.${name}`,
|
||||
description,
|
||||
type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse dimensions from the schema file
|
||||
*/
|
||||
function parseDimensions(schemaContent: string): DimensionInfo[] {
|
||||
const dimensions: DimensionInfo[] = [];
|
||||
|
||||
// Extract dimensions from FeedbackRecords cube
|
||||
const feedbackRecordsMatch = schemaContent.match(/cube\(`FeedbackRecords`,\s*\{([\s\S]*?)\n\}\);/);
|
||||
if (feedbackRecordsMatch) {
|
||||
const feedbackRecordsDimensions = parseDimensionsFromCube(feedbackRecordsMatch[1], "FeedbackRecords");
|
||||
dimensions.push(...feedbackRecordsDimensions);
|
||||
}
|
||||
|
||||
// Extract dimensions from TopicsUnnested cube
|
||||
const topicsUnnestedMatch = schemaContent.match(/cube\(`TopicsUnnested`,\s*\{([\s\S]*?)\n\}\);/);
|
||||
if (topicsUnnestedMatch) {
|
||||
const topicsDimensions = parseDimensionsFromCube(topicsUnnestedMatch[1], "TopicsUnnested");
|
||||
dimensions.push(...topicsDimensions);
|
||||
}
|
||||
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse the schema file
|
||||
*/
|
||||
export function parseSchemaFile(): {
|
||||
measures: MeasureInfo[];
|
||||
dimensions: DimensionInfo[];
|
||||
} {
|
||||
try {
|
||||
const schemaContent = fs.readFileSync(SCHEMA_FILE_PATH, "utf-8");
|
||||
const measures = parseMeasures(schemaContent);
|
||||
const dimensions = parseDimensions(schemaContent);
|
||||
|
||||
return { measures, dimensions };
|
||||
} catch (error) {
|
||||
console.error("Error parsing schema file:", error);
|
||||
// Fallback to empty arrays if parsing fails
|
||||
return { measures: [], dimensions: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the schema context string for AI query generation
|
||||
*/
|
||||
export function generateSchemaContext(): string {
|
||||
const { measures, dimensions } = parseSchemaFile();
|
||||
const CUBE_NAME = "FeedbackRecords";
|
||||
|
||||
const measuresList = measures.map((m) => `- ${CUBE_NAME}.${m.name} - ${m.description}`).join("\n");
|
||||
|
||||
const dimensionsList = dimensions
|
||||
.map((d) => {
|
||||
const typeLabel = d.type === "time" ? " (time dimension)" : ` (${d.type})`;
|
||||
// Dimensions from TopicsUnnested already have the cube prefix
|
||||
const fullName = d.name.includes(".") ? d.name : `${CUBE_NAME}.${d.name}`;
|
||||
return `- ${fullName} - ${d.description}${typeLabel}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const categoricalDimensions = dimensions
|
||||
.filter(
|
||||
(d) =>
|
||||
d.type === "string" &&
|
||||
!d.name.includes("responseId") &&
|
||||
!d.name.includes("userIdentifier") &&
|
||||
!d.name.includes("feedbackRecordId")
|
||||
)
|
||||
.map((d) => (d.name.includes(".") ? d.name : `${CUBE_NAME}.${d.name}`))
|
||||
.join(", ");
|
||||
|
||||
return `
|
||||
You are a CubeJS query generator. Your task is to convert natural language requests into valid CubeJS query JSON objects.
|
||||
|
||||
Available Cubes: ${CUBE_NAME}, TopicsUnnested
|
||||
|
||||
MEASURES (use these in the "measures" array):
|
||||
${measuresList}
|
||||
|
||||
DIMENSIONS (use these in the "dimensions" array):
|
||||
${dimensionsList}
|
||||
|
||||
TIME DIMENSIONS:
|
||||
- ${CUBE_NAME}.collectedAt can be used with granularity: 'day', 'week', 'month', 'year'
|
||||
- Use "timeDimensions" array for time-based queries with dateRange like "last 7 days", "last 30 days", "this month", etc.
|
||||
|
||||
CHART TYPE SUGGESTIONS:
|
||||
- If query has timeDimensions → suggest "bar" or "line"
|
||||
- If query has categorical dimensions (${categoricalDimensions}) → suggest "donut" or "bar"
|
||||
- If query has only measures → suggest "kpi"
|
||||
- If query compares multiple measures → suggest "bar"
|
||||
|
||||
FILTERS:
|
||||
- Use "filters" array to include/exclude records based on dimension values
|
||||
- Filter format: { "member": "CubeName.dimensionName", "operator": "operator" } OR { "member": "CubeName.dimensionName", "operator": "operator", "values": [...] }
|
||||
- Common operators:
|
||||
* "set" - dimension is not null/empty (Set "values" to null)
|
||||
Example: { "member": "${CUBE_NAME}.emotion", "operator": "set", "values": null }
|
||||
* "notSet" - dimension is null/empty (Set "values" to null)
|
||||
Example: { "member": "${CUBE_NAME}.emotion", "operator": "notSet", "values": null }
|
||||
* "equals" - exact match (REQUIRES "values" field)
|
||||
Example: { "member": "${CUBE_NAME}.emotion", "operator": "equals", "values": ["happy"] }
|
||||
* "notEquals" - not equal (REQUIRES "values" field)
|
||||
Example: { "member": "${CUBE_NAME}.emotion", "operator": "notEquals", "values": ["sad"] }
|
||||
* "contains" - contains text (REQUIRES "values" field)
|
||||
Example: { "member": "${CUBE_NAME}.emotion", "operator": "contains", "values": ["happy"] }
|
||||
- Examples for common user requests:
|
||||
* "only records with emotion" or "for records that have emotion" → { "member": "${CUBE_NAME}.emotion", "operator": "set", "values": null }
|
||||
* "exclude records without emotion" or "do not include records without emotion" → { "member": "${CUBE_NAME}.emotion", "operator": "set", "values": null }
|
||||
* "exclude records with emotion" or "do not include records with emotion" → { "member": "${CUBE_NAME}.emotion", "operator": "notSet", "values": null }
|
||||
* "only happy emotions" → { "member": "${CUBE_NAME}.emotion", "operator": "equals", "values": ["happy"] }
|
||||
|
||||
IMPORTANT RULES:
|
||||
1. Always return valid JSON only, no markdown or code blocks
|
||||
2. Use exact measure/dimension names as listed above
|
||||
3. Include "chartType" field: "bar", "line", "donut", "kpi", or "area"
|
||||
4. For time queries, use timeDimensions array with granularity and dateRange
|
||||
5. Return format: { "measures": [...], "dimensions": [...], "timeDimensions": [...], "filters": [...], "chartType": "..." }
|
||||
6. If user asks about trends over time, use timeDimensions
|
||||
7. If user asks "by X", add X as a dimension
|
||||
8. If user asks for counts or totals, use ${CUBE_NAME}.count
|
||||
9. If user asks for NPS, use ${CUBE_NAME}.npsScore
|
||||
10. If user asks about topics, use TopicsUnnested.topic (NOT ${CUBE_NAME}.topic)
|
||||
11. CRITICAL: If user says "only records with X", "exclude records without X", or "for records that have X", add a filter with operator "set" for that dimension
|
||||
12. CRITICAL: If user says "exclude records with X", "do not include records with X", or "without X", add a filter with operator "notSet" for that dimension
|
||||
13. Always include filters when user explicitly mentions including/excluding records based on dimension values
|
||||
`.trim();
|
||||
}
|
||||
|
||||
export const CUBE_NAME = "FeedbackRecords";
|
||||
39
apps/web/app/api/analytics/_lib/types.ts
Normal file
39
apps/web/app/api/analytics/_lib/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* TypeScript types for the Analytics API
|
||||
*/
|
||||
|
||||
export interface TimeDimension {
|
||||
dimension: string;
|
||||
granularity?: "day" | "week" | "month" | "year";
|
||||
dateRange?: string;
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
member: string;
|
||||
operator:
|
||||
| "equals"
|
||||
| "notEquals"
|
||||
| "contains"
|
||||
| "notContains"
|
||||
| "set"
|
||||
| "notSet"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "lt"
|
||||
| "lte";
|
||||
values?: string[] | null;
|
||||
}
|
||||
|
||||
export interface CubeQuery {
|
||||
measures: string[];
|
||||
dimensions?: string[];
|
||||
timeDimensions?: TimeDimension[];
|
||||
filters?: Filter[];
|
||||
}
|
||||
|
||||
export interface AnalyticsResponse {
|
||||
query: CubeQuery;
|
||||
chartType: "bar" | "line" | "donut" | "kpi" | "area" | "pie";
|
||||
data?: Record<string, any>[];
|
||||
error?: string;
|
||||
}
|
||||
156
apps/web/app/api/analytics/_schema/FeedbackRecords.js
Normal file
156
apps/web/app/api/analytics/_schema/FeedbackRecords.js
Normal file
@@ -0,0 +1,156 @@
|
||||
cube(`FeedbackRecords`, {
|
||||
sql: `SELECT * FROM feedback_records`,
|
||||
|
||||
measures: {
|
||||
count: {
|
||||
type: `count`,
|
||||
description: `Total number of feedback responses`,
|
||||
},
|
||||
|
||||
promoterCount: {
|
||||
type: `count`,
|
||||
filters: [{ sql: `${CUBE}.value_number >= 9` }],
|
||||
description: `Number of promoters (NPS score 9-10)`,
|
||||
},
|
||||
|
||||
detractorCount: {
|
||||
type: `count`,
|
||||
filters: [{ sql: `${CUBE}.value_number <= 6` }],
|
||||
description: `Number of detractors (NPS score 0-6)`,
|
||||
},
|
||||
|
||||
passiveCount: {
|
||||
type: `count`,
|
||||
filters: [{ sql: `${CUBE}.value_number >= 7 AND ${CUBE}.value_number <= 8` }],
|
||||
description: `Number of passives (NPS score 7-8)`,
|
||||
},
|
||||
|
||||
npsScore: {
|
||||
type: `number`,
|
||||
sql: `
|
||||
CASE
|
||||
WHEN COUNT(*) = 0 THEN 0
|
||||
ELSE ROUND(
|
||||
(
|
||||
(COUNT(CASE WHEN ${CUBE}.value_number >= 9 THEN 1 END)::numeric -
|
||||
COUNT(CASE WHEN ${CUBE}.value_number <= 6 THEN 1 END)::numeric)
|
||||
/ COUNT(*)::numeric
|
||||
) * 100,
|
||||
2
|
||||
)
|
||||
END
|
||||
`,
|
||||
description: `Net Promoter Score: ((Promoters - Detractors) / Total) * 100`,
|
||||
},
|
||||
|
||||
averageScore: {
|
||||
type: `avg`,
|
||||
sql: `${CUBE}.value_number`,
|
||||
description: `Average NPS score`,
|
||||
},
|
||||
},
|
||||
|
||||
dimensions: {
|
||||
id: {
|
||||
sql: `id`,
|
||||
type: `string`,
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
sentiment: {
|
||||
sql: `sentiment`,
|
||||
type: `string`,
|
||||
description: `Sentiment extracted from metadata JSONB field`,
|
||||
},
|
||||
|
||||
sourceType: {
|
||||
sql: `source_type`,
|
||||
type: `string`,
|
||||
description: `Source type of the feedback (e.g., nps_campaign, survey)`,
|
||||
},
|
||||
|
||||
sourceName: {
|
||||
sql: `source_name`,
|
||||
type: `string`,
|
||||
description: `Human-readable name of the source`,
|
||||
},
|
||||
|
||||
fieldType: {
|
||||
sql: `field_type`,
|
||||
type: `string`,
|
||||
description: `Type of feedback field (e.g., nps, text, rating)`,
|
||||
},
|
||||
|
||||
collectedAt: {
|
||||
sql: `collected_at`,
|
||||
type: `time`,
|
||||
description: `Timestamp when the feedback was collected`,
|
||||
},
|
||||
|
||||
npsValue: {
|
||||
sql: `value_number`,
|
||||
type: `number`,
|
||||
description: `Raw NPS score value (0-10)`,
|
||||
},
|
||||
|
||||
responseId: {
|
||||
sql: `response_id`,
|
||||
type: `string`,
|
||||
description: `Unique identifier linking related feedback records`,
|
||||
},
|
||||
|
||||
userIdentifier: {
|
||||
sql: `user_identifier`,
|
||||
type: `string`,
|
||||
description: `Identifier of the user who provided feedback`,
|
||||
},
|
||||
|
||||
emotion: {
|
||||
sql: `emotion`,
|
||||
type: `string`,
|
||||
description: `Emotion extracted from metadata JSONB field`,
|
||||
},
|
||||
},
|
||||
|
||||
joins: {
|
||||
TopicsUnnested: {
|
||||
sql: `${CUBE}.id = ${TopicsUnnested}.feedback_record_id`,
|
||||
relationship: `hasMany`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
cube(`TopicsUnnested`, {
|
||||
sql: `
|
||||
SELECT
|
||||
fr.id as feedback_record_id,
|
||||
topic_elem.topic
|
||||
FROM feedback_records fr
|
||||
CROSS JOIN LATERAL jsonb_array_elements_text(COALESCE(fr.metadata->'topics', '[]'::jsonb)) AS topic_elem(topic)
|
||||
`,
|
||||
|
||||
measures: {
|
||||
count: {
|
||||
type: `count`,
|
||||
},
|
||||
},
|
||||
|
||||
dimensions: {
|
||||
id: {
|
||||
sql: `feedback_record_id || '-' || topic`,
|
||||
type: `string`,
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
feedbackRecordId: {
|
||||
sql: `feedback_record_id`,
|
||||
type: `string`,
|
||||
},
|
||||
|
||||
topic: {
|
||||
sql: `topic`,
|
||||
type: `string`,
|
||||
description: `Individual topic from the topics array`,
|
||||
},
|
||||
},
|
||||
});
|
||||
246
apps/web/app/api/analytics/query/route.ts
Normal file
246
apps/web/app/api/analytics/query/route.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import OpenAI from "openai";
|
||||
import { z } from "zod";
|
||||
import { executeQuery } from "../_lib/cube-client";
|
||||
import { CUBE_NAME, generateSchemaContext } from "../_lib/schema-parser";
|
||||
|
||||
const schema = z.object({
|
||||
measures: z.array(z.string()).describe("List of measures to query"),
|
||||
dimensions: z.array(z.string()).nullable().describe("List of dimensions to query"),
|
||||
timeDimensions: z
|
||||
.array(
|
||||
z.object({
|
||||
dimension: z.string(),
|
||||
granularity: z.enum(["day", "week", "month", "year"]).nullable(),
|
||||
dateRange: z.string().nullable(),
|
||||
})
|
||||
)
|
||||
.nullable()
|
||||
.describe("Time dimensions with granularity and date range"),
|
||||
chartType: z
|
||||
.enum(["bar", "line", "donut", "kpi", "area", "pie"])
|
||||
.describe("Suggested chart type for visualization"),
|
||||
filters: z
|
||||
.array(
|
||||
z.object({
|
||||
member: z.string(),
|
||||
operator: z.enum([
|
||||
"equals",
|
||||
"notEquals",
|
||||
"contains",
|
||||
"notContains",
|
||||
"set",
|
||||
"notSet",
|
||||
"gt",
|
||||
"gte",
|
||||
"lt",
|
||||
"lte",
|
||||
]),
|
||||
values: z.array(z.string()).nullable(),
|
||||
})
|
||||
)
|
||||
.nullable()
|
||||
.describe("Filters to apply to the query"),
|
||||
});
|
||||
|
||||
// Generate schema context dynamically from the schema file
|
||||
const SCHEMA_CONTEXT = generateSchemaContext();
|
||||
|
||||
// JSON Schema for OpenAI structured outputs (manually created to avoid zod-to-json-schema dependency)
|
||||
const jsonSchema = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
measures: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "List of measures to query",
|
||||
},
|
||||
dimensions: {
|
||||
anyOf: [{ type: "array", items: { type: "string" } }, { type: "null" }],
|
||||
description: "List of dimensions to query",
|
||||
},
|
||||
timeDimensions: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
dimension: { type: "string" },
|
||||
granularity: {
|
||||
anyOf: [{ type: "string", enum: ["day", "week", "month", "year"] }, { type: "null" }],
|
||||
},
|
||||
dateRange: {
|
||||
anyOf: [{ type: "string" }, { type: "null" }],
|
||||
},
|
||||
},
|
||||
required: ["dimension", "granularity", "dateRange"],
|
||||
},
|
||||
},
|
||||
{ type: "null" },
|
||||
],
|
||||
description: "Time dimensions with granularity and date range",
|
||||
},
|
||||
chartType: {
|
||||
type: "string",
|
||||
enum: ["bar", "line", "donut", "kpi", "area", "pie"],
|
||||
description: "Suggested chart type for visualization",
|
||||
},
|
||||
filters: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
member: { type: "string" },
|
||||
operator: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"equals",
|
||||
"notEquals",
|
||||
"contains",
|
||||
"notContains",
|
||||
"set",
|
||||
"notSet",
|
||||
"gt",
|
||||
"gte",
|
||||
"lt",
|
||||
"lte",
|
||||
],
|
||||
},
|
||||
values: {
|
||||
anyOf: [{ type: "array", items: { type: "string" } }, { type: "null" }],
|
||||
},
|
||||
},
|
||||
required: ["member", "operator", "values"],
|
||||
},
|
||||
},
|
||||
{ type: "null" },
|
||||
],
|
||||
description: "Filters to apply to the query",
|
||||
},
|
||||
},
|
||||
required: ["measures", "dimensions", "timeDimensions", "chartType", "filters"],
|
||||
} as const;
|
||||
|
||||
// Initialize OpenAI client
|
||||
const getOpenAIClient = () => {
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
throw new Error("OPENAI_API_KEY is not configured");
|
||||
}
|
||||
return new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { prompt, executeQuery: shouldExecuteQuery = true } = await request.json();
|
||||
|
||||
if (!prompt || typeof prompt !== "string") {
|
||||
return NextResponse.json({ error: "Prompt is required and must be a string" }, { status: 400 });
|
||||
}
|
||||
|
||||
const openai = getOpenAIClient();
|
||||
|
||||
// Generate Cube.js query using OpenAI structured outputs
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: SCHEMA_CONTEXT },
|
||||
{ role: "user", content: `User request: "${prompt}"` },
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "generate_cube_query",
|
||||
description: "Generate a Cube.js query based on the user request",
|
||||
parameters: jsonSchema,
|
||||
strict: true, // Enable structured outputs
|
||||
},
|
||||
},
|
||||
],
|
||||
tool_choice: { type: "function", function: { name: "generate_cube_query" } },
|
||||
});
|
||||
|
||||
const toolCall = completion.choices[0]?.message?.tool_calls?.[0];
|
||||
if (toolCall?.function.name !== "generate_cube_query") {
|
||||
throw new Error("Failed to generate structured output from OpenAI");
|
||||
}
|
||||
|
||||
const query = JSON.parse(toolCall.function.arguments);
|
||||
|
||||
// Validate with zod schema (for type safety)
|
||||
const validatedQuery = schema.parse(query);
|
||||
|
||||
// Validate required fields (measures should minimally be present if not specified, default to count)
|
||||
if (!validatedQuery.measures || validatedQuery.measures.length === 0) {
|
||||
validatedQuery.measures = [`${CUBE_NAME}.count`];
|
||||
}
|
||||
|
||||
// Extract chartType (for UI purposes only, not part of CubeJS query)
|
||||
const { chartType, ...cubeQuery } = validatedQuery;
|
||||
|
||||
// Clean up null/empty values to conform to CubeJS expectations
|
||||
if (
|
||||
cubeQuery.dimensions === null ||
|
||||
(Array.isArray(cubeQuery.dimensions) && cubeQuery.dimensions.length === 0)
|
||||
) {
|
||||
delete (cubeQuery as any).dimensions;
|
||||
}
|
||||
if (!cubeQuery.filters || cubeQuery.filters.length === 0) {
|
||||
delete (cubeQuery as any).filters;
|
||||
} else {
|
||||
// Clean up null values in filters
|
||||
cubeQuery.filters = cubeQuery.filters.map((f: any) => {
|
||||
const newFilter: any = { ...f };
|
||||
if (newFilter.values === null) delete newFilter.values;
|
||||
return newFilter;
|
||||
});
|
||||
}
|
||||
if (cubeQuery.timeDimensions === null) {
|
||||
delete (cubeQuery as any).timeDimensions;
|
||||
} else if (Array.isArray(cubeQuery.timeDimensions)) {
|
||||
// Filter out null properties in timeDimensions objects
|
||||
cubeQuery.timeDimensions = cubeQuery.timeDimensions.map((td: any) => {
|
||||
const newTd: any = { ...td };
|
||||
if (newTd.granularity === null) delete newTd.granularity;
|
||||
if (newTd.dateRange === null) delete newTd.dateRange;
|
||||
return newTd;
|
||||
});
|
||||
}
|
||||
|
||||
// Execute query if requested (default: true)
|
||||
let data: Record<string, any>[] | undefined;
|
||||
if (shouldExecuteQuery) {
|
||||
try {
|
||||
data = await executeQuery(cubeQuery);
|
||||
} catch (queryError: any) {
|
||||
console.error("Error executing Cube.js query:", queryError);
|
||||
// Return the query even if execution fails, so client can retry
|
||||
return NextResponse.json(
|
||||
{
|
||||
query: cubeQuery,
|
||||
chartType,
|
||||
error: `Failed to execute query: ${queryError.message || "Unknown error"}`,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
query: cubeQuery,
|
||||
chartType,
|
||||
data,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error generating query:", error);
|
||||
return NextResponse.json({ error: error.message || "Failed to generate query" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
"@aws-sdk/s3-presigned-post": "3.879.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.879.0",
|
||||
"@boxyhq/saml-jackson": "1.52.2",
|
||||
"@cubejs-client/core": "1.6.6",
|
||||
"@dnd-kit/core": "6.3.1",
|
||||
"@dnd-kit/modifiers": "9.0.0",
|
||||
"@dnd-kit/sortable": "10.0.0",
|
||||
@@ -79,6 +80,7 @@
|
||||
"@ungap/structured-clone": "1.3.0",
|
||||
"@vercel/functions": "2.2.8",
|
||||
"@vercel/og": "0.8.5",
|
||||
"openai": "^4.0.0",
|
||||
"bcryptjs": "3.0.2",
|
||||
"boring-avatars": "2.0.1",
|
||||
"class-variance-authority": "0.7.1",
|
||||
@@ -111,10 +113,12 @@
|
||||
"prismjs": "1.30.0",
|
||||
"qr-code-styling": "1.9.2",
|
||||
"qrcode": "1.5.4",
|
||||
"react": "19.2.3",
|
||||
"react-calendar": "5.1.0",
|
||||
"react-colorful": "5.6.1",
|
||||
"react-confetti": "6.4.0",
|
||||
"react-day-picker": "9.6.7",
|
||||
"react-dom": "19.2.3",
|
||||
"react-hook-form": "7.56.2",
|
||||
"react-hot-toast": "2.5.2",
|
||||
"react-i18next": "15.7.3",
|
||||
@@ -133,9 +137,7 @@
|
||||
"webpack": "5.99.8",
|
||||
"xlsx": "file:vendor/xlsx-0.20.3.tgz",
|
||||
"zod": "3.24.4",
|
||||
"zod-openapi": "4.2.4",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
"zod-openapi": "4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
|
||||
117
pnpm-lock.yaml
generated
117
pnpm-lock.yaml
generated
@@ -134,6 +134,9 @@ importers:
|
||||
'@boxyhq/saml-jackson':
|
||||
specifier: 1.52.2
|
||||
version: 1.52.2(socks@2.8.7)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.8.3))
|
||||
'@cubejs-client/core':
|
||||
specifier: 1.6.6
|
||||
version: 1.6.6(encoding@0.1.13)
|
||||
'@dnd-kit/core':
|
||||
specifier: 6.3.1
|
||||
version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -383,6 +386,9 @@ importers:
|
||||
nodemailer:
|
||||
specifier: 7.0.11
|
||||
version: 7.0.11
|
||||
openai:
|
||||
specifier: ^4.0.0
|
||||
version: 4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.24.4)
|
||||
otplib:
|
||||
specifier: 12.0.1
|
||||
version: 12.0.1
|
||||
@@ -473,6 +479,9 @@ importers:
|
||||
zod-openapi:
|
||||
specifier: 4.2.4
|
||||
version: 4.2.4(zod@3.24.4)
|
||||
zod-to-json-schema:
|
||||
specifier: ^3.23.5
|
||||
version: 3.25.1(zod@3.24.4)
|
||||
devDependencies:
|
||||
'@formbricks/config-typescript':
|
||||
specifier: workspace:*
|
||||
@@ -1753,6 +1762,9 @@ packages:
|
||||
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@cubejs-client/core@1.6.6':
|
||||
resolution: {integrity: sha512-JWEVQaaS7y6Y3Nhe4Lcjl8coP5eXIgYZQTb2WMlLdMluSGM4mWOSFpAis77lAPV+nybFvRf9ri+PjCGqGJXH5g==}
|
||||
|
||||
'@date-fns/tz@1.2.0':
|
||||
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
|
||||
|
||||
@@ -5235,6 +5247,9 @@ packages:
|
||||
'@types/node-fetch@2.6.13':
|
||||
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
|
||||
|
||||
'@types/node@18.19.130':
|
||||
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
|
||||
|
||||
'@types/node@22.15.18':
|
||||
resolution: {integrity: sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==}
|
||||
|
||||
@@ -6488,6 +6503,9 @@ packages:
|
||||
core-js-compat@3.47.0:
|
||||
resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==}
|
||||
|
||||
core-js@3.48.0:
|
||||
resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==}
|
||||
|
||||
core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
|
||||
@@ -6503,6 +6521,9 @@ packages:
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
cross-fetch@3.2.0:
|
||||
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -7359,10 +7380,17 @@ packages:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data-encoder@1.7.2:
|
||||
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||
|
||||
form-data@4.0.5:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
formdata-node@4.4.1:
|
||||
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
|
||||
engines: {node: '>= 12.20'}
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
@@ -8890,6 +8918,18 @@ packages:
|
||||
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
openai@4.104.0:
|
||||
resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
ws: ^8.18.0
|
||||
zod: ^3.23.8
|
||||
peerDependenciesMeta:
|
||||
ws:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
openid-client@5.7.1:
|
||||
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
|
||||
|
||||
@@ -9394,6 +9434,9 @@ packages:
|
||||
quick-format-unescaped@4.0.4:
|
||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||
|
||||
ramda@0.27.2:
|
||||
resolution: {integrity: sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==}
|
||||
|
||||
randombytes@2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
|
||||
@@ -10636,6 +10679,9 @@ packages:
|
||||
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
undici-types@5.26.5:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
@@ -10675,6 +10721,9 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
url-search-params-polyfill@7.0.1:
|
||||
resolution: {integrity: sha512-bAw7L2E+jn9XHG5P9zrPnHdO0yJub4U+yXJOdpcpkr7OBd9T8oll4lUos0iSGRcDvfZoLUKfx9a6aNmIhJ4+mQ==}
|
||||
|
||||
url-template@2.0.8:
|
||||
resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
|
||||
|
||||
@@ -10883,6 +10932,10 @@ packages:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3:
|
||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
@@ -11123,6 +11176,11 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.21.4
|
||||
|
||||
zod-to-json-schema@3.25.1:
|
||||
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
|
||||
peerDependencies:
|
||||
zod: ^3.25 || ^4
|
||||
|
||||
zod@3.24.4:
|
||||
resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==}
|
||||
|
||||
@@ -12863,6 +12921,17 @@ snapshots:
|
||||
|
||||
'@csstools/css-tokenizer@3.0.4': {}
|
||||
|
||||
'@cubejs-client/core@1.6.6(encoding@0.1.13)':
|
||||
dependencies:
|
||||
core-js: 3.48.0
|
||||
cross-fetch: 3.2.0(encoding@0.1.13)
|
||||
dayjs: 1.11.19
|
||||
ramda: 0.27.2
|
||||
url-search-params-polyfill: 7.0.1
|
||||
uuid: 11.1.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
'@date-fns/tz@1.2.0': {}
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1(react@19.2.3)':
|
||||
@@ -16699,6 +16768,10 @@ snapshots:
|
||||
'@types/node': 22.15.18
|
||||
form-data: 4.0.5
|
||||
|
||||
'@types/node@18.19.130':
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/node@22.15.18':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
@@ -17565,7 +17638,6 @@ snapshots:
|
||||
agentkeepalive@4.6.0:
|
||||
dependencies:
|
||||
humanize-ms: 1.2.1
|
||||
optional: true
|
||||
|
||||
aggregate-error@3.1.0:
|
||||
dependencies:
|
||||
@@ -18191,6 +18263,8 @@ snapshots:
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
|
||||
core-js@3.48.0: {}
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
|
||||
cors@2.8.5:
|
||||
@@ -18205,6 +18279,12 @@ snapshots:
|
||||
'@epic-web/invariant': 1.0.0
|
||||
cross-spawn: 7.0.6
|
||||
|
||||
cross-fetch@3.2.0(encoding@0.1.13):
|
||||
dependencies:
|
||||
node-fetch: 2.7.0(encoding@0.1.13)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
@@ -19267,6 +19347,8 @@ snapshots:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
form-data-encoder@1.7.2: {}
|
||||
|
||||
form-data@4.0.5:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
@@ -19275,6 +19357,11 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
formdata-node@4.4.1:
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 4.0.0-beta.3
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
dependencies:
|
||||
fetch-blob: 3.2.0
|
||||
@@ -19651,7 +19738,6 @@ snapshots:
|
||||
humanize-ms@1.2.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optional: true
|
||||
|
||||
husky@9.1.7: {}
|
||||
|
||||
@@ -20869,6 +20955,21 @@ snapshots:
|
||||
is-docker: 2.2.1
|
||||
is-wsl: 2.2.0
|
||||
|
||||
openai@4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.24.4):
|
||||
dependencies:
|
||||
'@types/node': 18.19.130
|
||||
'@types/node-fetch': 2.6.13
|
||||
abort-controller: 3.0.0
|
||||
agentkeepalive: 4.6.0
|
||||
form-data-encoder: 1.7.2
|
||||
formdata-node: 4.4.1
|
||||
node-fetch: 2.7.0(encoding@0.1.13)
|
||||
optionalDependencies:
|
||||
ws: 8.18.3
|
||||
zod: 3.24.4
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
openid-client@5.7.1:
|
||||
dependencies:
|
||||
jose: 4.15.9
|
||||
@@ -21338,6 +21439,8 @@ snapshots:
|
||||
|
||||
quick-format-unescaped@4.0.4: {}
|
||||
|
||||
ramda@0.27.2: {}
|
||||
|
||||
randombytes@2.1.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
@@ -22837,6 +22940,8 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
which-boxed-primitive: 1.1.1
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
unicode-trie@2.0.0:
|
||||
@@ -22909,6 +23014,8 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
url-search-params-polyfill@7.0.1: {}
|
||||
|
||||
url-template@2.0.8: {}
|
||||
|
||||
use-callback-ref@1.3.3(@types/react@19.2.1)(react@19.2.1):
|
||||
@@ -23247,6 +23354,8 @@ snapshots:
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webidl-conversions@7.0.0: {}
|
||||
@@ -23496,4 +23605,8 @@ snapshots:
|
||||
dependencies:
|
||||
zod: 3.24.4
|
||||
|
||||
zod-to-json-schema@3.25.1(zod@3.24.4):
|
||||
dependencies:
|
||||
zod: 3.24.4
|
||||
|
||||
zod@3.24.4: {}
|
||||
|
||||
Reference in New Issue
Block a user