mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-08 02:43:06 -05:00
add document search
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { embed, generateText } from "ai";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||||
|
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||||
|
import { embeddingsModel, llmModel } from "@formbricks/lib/ai";
|
||||||
|
import { findNearestDocuments, getDocumentsByTypeAndReferenceId } from "@formbricks/lib/document/service";
|
||||||
|
import { getQuestionResponseReferenceId } from "@formbricks/lib/document/utils";
|
||||||
|
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||||
|
import { ZId } from "@formbricks/types/environment";
|
||||||
|
|
||||||
|
const ZSearchDocuments = z.object({
|
||||||
|
environmentId: ZId,
|
||||||
|
searchTerm: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const searchDocumentsAction = authenticatedActionClient
|
||||||
|
.schema(ZSearchDocuments)
|
||||||
|
.action(async ({ ctx, parsedInput }) => {
|
||||||
|
await checkAuthorization({
|
||||||
|
userId: ctx.user.id,
|
||||||
|
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||||
|
rules: ["environment", "read"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { text } = await generateText({
|
||||||
|
model: llmModel,
|
||||||
|
prompt: `Generate 4 example survey feedback responses from users in one line, separated by comma for the following topic: ${parsedInput.searchTerm}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { embedding } = await embed({
|
||||||
|
model: embeddingsModel,
|
||||||
|
value: text,
|
||||||
|
});
|
||||||
|
|
||||||
|
const documents = findNearestDocuments(parsedInput.environmentId, embedding, 10);
|
||||||
|
|
||||||
|
return documents;
|
||||||
|
});
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { TDocument } from "@formbricks/types/documents";
|
||||||
|
import { Button } from "@formbricks/ui/Button";
|
||||||
|
import { Card } from "@formbricks/ui/Card";
|
||||||
|
import { Input } from "@formbricks/ui/Input";
|
||||||
|
import { searchDocumentsAction } from "../actions";
|
||||||
|
|
||||||
|
interface DocumentSearchProps {
|
||||||
|
environmentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocumentSearch = ({ environmentId }: DocumentSearchProps) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [documents, setDocuments] = useState<TDocument[]>([]);
|
||||||
|
|
||||||
|
const searchDocuments = async () => {
|
||||||
|
const documents = await searchDocumentsAction({ environmentId, searchTerm });
|
||||||
|
if (documents?.data) {
|
||||||
|
setDocuments(documents.data);
|
||||||
|
} else {
|
||||||
|
console.error(documents);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
searchDocuments();
|
||||||
|
}}
|
||||||
|
className="flex w-full space-x-2">
|
||||||
|
<Input
|
||||||
|
placeholder="What are users thinking the performance of my app?"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(v) => setSearchTerm(v.target.value)}
|
||||||
|
/>
|
||||||
|
<Button className="h-10">Search</Button>
|
||||||
|
</form>
|
||||||
|
<div className="flex-col space-y-4">
|
||||||
|
{documents.map((document) => (
|
||||||
|
<div className="overflow-hidden rounded-lg bg-white shadow">
|
||||||
|
<div className="whitespace-pre-wrap px-4 py-5 sm:p-6">
|
||||||
|
<p>{document.text}</p>
|
||||||
|
<hr className="my-4 text-slate-300" />
|
||||||
|
<p className="text-xs">Survey Response</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { LoadingSpinner } from "@formbricks/ui/LoadingSpinner";
|
||||||
|
|
||||||
|
const Loading = () => {
|
||||||
|
return <LoadingSpinner />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loading;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { DocumentSearch } from "@/app/(app)/environments/[environmentId]/documents/components/DocumentSearch";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { authOptions } from "@formbricks/lib/authOptions";
|
||||||
|
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||||
|
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||||
|
import { getUser } from "@formbricks/lib/user/service";
|
||||||
|
import { TTemplateRole } from "@formbricks/types/templates";
|
||||||
|
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||||
|
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Your Surveys",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SurveyTemplateProps {
|
||||||
|
params: {
|
||||||
|
environmentId: string;
|
||||||
|
};
|
||||||
|
searchParams: {
|
||||||
|
role?: TTemplateRole;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Page = async ({ params, searchParams }: SurveyTemplateProps) => {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
throw new Error("Session not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await getUser(session.user.id);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContentWrapper>
|
||||||
|
<PageHeader pageTitle="Documents" />
|
||||||
|
<DocumentSearch environmentId={params.environmentId} />
|
||||||
|
</PageContentWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ZDocumentCreateInput,
|
ZDocumentCreateInput,
|
||||||
ZDocumentType,
|
ZDocumentType,
|
||||||
} from "@formbricks/types/documents";
|
} from "@formbricks/types/documents";
|
||||||
|
import { ZId } from "@formbricks/types/environment";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
import { cache } from "../cache";
|
import { cache } from "../cache";
|
||||||
import { validateInputs } from "../utils/validate";
|
import { validateInputs } from "../utils/validate";
|
||||||
@@ -72,9 +73,9 @@ export const getDocumentsByTypeAndReferenceId = reactCache(
|
|||||||
text,
|
text,
|
||||||
"referenceId",
|
"referenceId",
|
||||||
vector::text
|
vector::text
|
||||||
FROM "Document" e
|
FROM "Document" d
|
||||||
WHERE e."type" = ${type}::"DocumentType"
|
WHERE d."type" = ${type}::"DocumentType"
|
||||||
AND e."referenceId" = ${referenceId}
|
AND d."referenceId" = ${referenceId}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const documents = prismaDocuments.map((prismaDocument) => {
|
const documents = prismaDocuments.map((prismaDocument) => {
|
||||||
@@ -104,3 +105,45 @@ export const getDocumentsByTypeAndReferenceId = reactCache(
|
|||||||
}
|
}
|
||||||
)()
|
)()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const findNearestDocuments = async (
|
||||||
|
environmentId: string,
|
||||||
|
vector: number[],
|
||||||
|
limit: number = 5
|
||||||
|
): Promise<TDocument[]> => {
|
||||||
|
validateInputs([environmentId, ZId]);
|
||||||
|
const threshold = 0.8; //0.2;
|
||||||
|
// Convert the embedding array to a JSON-like string representation
|
||||||
|
const vectorString = `[${vector.join(",")}]`;
|
||||||
|
|
||||||
|
// Execute raw SQL query to find nearest neighbors and exclude the vector column
|
||||||
|
const prismaDocuments: TPrismaDocument[] = await prisma.$queryRaw`
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
created_at AS "createdAt",
|
||||||
|
updated_at AS "updatedAt",
|
||||||
|
type,
|
||||||
|
text,
|
||||||
|
"referenceId",
|
||||||
|
vector::text
|
||||||
|
FROM "Document" d
|
||||||
|
WHERE d."environmentId" = ${environmentId}
|
||||||
|
AND d."vector" <=> ${vectorString}::vector(512) <= ${threshold}
|
||||||
|
ORDER BY d."vector" <=> ${vectorString}::vector(512)
|
||||||
|
LIMIT ${limit};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const documents = prismaDocuments.map((prismaDocument) => {
|
||||||
|
// Convert the string representation of the vector back to an array of numbers
|
||||||
|
const vector = prismaDocument.vector
|
||||||
|
.slice(1, -1) // Remove the surrounding square brackets
|
||||||
|
.split(",") // Split the string into an array of strings
|
||||||
|
.map(Number); // Convert each string to a number
|
||||||
|
return {
|
||||||
|
...prismaDocument,
|
||||||
|
vector,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return documents;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user