aboutsummaryrefslogtreecommitdiff
path: root/apps/web/src/components
diff options
context:
space:
mode:
authorDhravya <[email protected]>2024-04-10 12:01:17 -0700
committerDhravya <[email protected]>2024-04-10 12:01:17 -0700
commit13dfbe2e30ea026707b5797cbd0169425916ab28 (patch)
treedcda83f72fc19bbba27b2fe22ded0630a36f9b46 /apps/web/src/components
parentcleanups for production (diff)
downloadsupermemory-13dfbe2e30ea026707b5797cbd0169425916ab28.tar.xz
supermemory-13dfbe2e30ea026707b5797cbd0169425916ab28.zip
fix styling in extension, remove ai sidebar in extension
Diffstat (limited to 'apps/web/src/components')
-rw-r--r--apps/web/src/components/QueryAI.tsx139
-rw-r--r--apps/web/src/components/SearchResults.tsx38
2 files changed, 177 insertions, 0 deletions
diff --git a/apps/web/src/components/QueryAI.tsx b/apps/web/src/components/QueryAI.tsx
new file mode 100644
index 00000000..3cb14178
--- /dev/null
+++ b/apps/web/src/components/QueryAI.tsx
@@ -0,0 +1,139 @@
+'use client';
+
+import { Label } from './ui/label';
+import React, { useEffect, useState } from 'react';
+import { Input } from './ui/input';
+import { Button } from './ui/button';
+import SearchResults from './SearchResults';
+
+function QueryAI() {
+ const [searchResults, setSearchResults] = useState<string[]>([]);
+ const [isAiLoading, setIsAiLoading] = useState(false);
+
+ const [aiResponse, setAIResponse] = useState('');
+ const [input, setInput] = useState('');
+ const [toBeParsed, setToBeParsed] = useState('');
+
+ const handleStreamData = (newChunk: string) => {
+ // Append the new chunk to the existing data to be parsed
+ setToBeParsed((prev) => prev + newChunk);
+ };
+
+ useEffect(() => {
+ // Define a function to try parsing the accumulated data
+ const tryParseAccumulatedData = () => {
+ // Attempt to parse the "toBeParsed" state as JSON
+ try {
+ // Split the accumulated data by the known delimiter "\n\n"
+ const parts = toBeParsed.split('\n\n');
+ let remainingData = '';
+
+ // Process each part to extract JSON objects
+ parts.forEach((part, index) => {
+ try {
+ const parsedPart = JSON.parse(part.replace('data: ', '')); // Try to parse the part as JSON
+
+ // If the part is the last one and couldn't be parsed, keep it to accumulate more data
+ if (index === parts.length - 1 && !parsedPart) {
+ remainingData = part;
+ } else if (parsedPart && parsedPart.response) {
+ // If the part is parsable and has the "response" field, update the AI response state
+ setAIResponse((prev) => prev + parsedPart.response);
+ }
+ } catch (error) {
+ // If parsing fails and it's not the last part, it's a malformed JSON
+ if (index !== parts.length - 1) {
+ console.error('Malformed JSON part: ', part);
+ } else {
+ // If it's the last part, it may be incomplete, so keep it
+ remainingData = part;
+ }
+ }
+ });
+
+ // Update the toBeParsed state to only contain the unparsed remainder
+ if (remainingData !== toBeParsed) {
+ setToBeParsed(remainingData);
+ }
+ } catch (error) {
+ console.error('Error parsing accumulated data: ', error);
+ }
+ };
+
+ // Call the parsing function if there's data to be parsed
+ if (toBeParsed) {
+ tryParseAccumulatedData();
+ }
+ }, [toBeParsed]);
+
+ const getSearchResults = async (e: React.FormEvent<HTMLFormElement>) => {
+ e.preventDefault();
+ setIsAiLoading(true);
+
+ const sourcesResponse = await fetch(
+ `/api/query?sourcesOnly=true&q=${input}`,
+ );
+
+ const sourcesInJson = (await sourcesResponse.json()) as {
+ ids: string[];
+ };
+
+ setSearchResults(sourcesInJson.ids);
+
+ const response = await fetch(`/api/query?q=${input}`);
+
+ if (response.status !== 200) {
+ setIsAiLoading(false);
+ return;
+ }
+
+ if (response.body) {
+ let reader = response.body.getReader();
+ let decoder = new TextDecoder('utf-8');
+ let result = '';
+
+ // @ts-ignore
+ reader.read().then(function processText({ done, value }) {
+ if (done) {
+ // setSearchResults(JSON.parse(result.replace('data: ', '')));
+ // setIsAiLoading(false);
+ return;
+ }
+
+ handleStreamData(decoder.decode(value));
+
+ return reader.read().then(processText);
+ });
+ }
+ };
+
+ return (
+ <div className="w-full max-w-2xl mx-auto">
+ <form onSubmit={async (e) => await getSearchResults(e)} className="mt-8">
+ <Label htmlFor="searchInput">Ask your SuperMemory</Label>
+ <div className="flex flex-col md:flex-row md:w-full md:items-center space-y-2 md:space-y-0 md:space-x-2">
+ <Input
+ value={input}
+ onChange={(e) => setInput(e.target.value)}
+ placeholder="Search using AI... ✨"
+ id="searchInput"
+ />
+ <Button
+ disabled={isAiLoading}
+ className="max-w-min md:w-full"
+ type="submit"
+ variant="default"
+ >
+ Ask AI
+ </Button>
+ </div>
+ </form>
+
+ {searchResults && (
+ <SearchResults aiResponse={aiResponse} sources={searchResults} />
+ )}
+ </div>
+ );
+}
+
+export default QueryAI;
diff --git a/apps/web/src/components/SearchResults.tsx b/apps/web/src/components/SearchResults.tsx
new file mode 100644
index 00000000..0445d0b4
--- /dev/null
+++ b/apps/web/src/components/SearchResults.tsx
@@ -0,0 +1,38 @@
+'use client'
+
+import React from 'react';
+import { Card, CardContent } from './ui/card';
+import Markdown from 'react-markdown';
+import remarkGfm from 'remark-gfm'
+
+function SearchResults({
+ aiResponse,
+ sources,
+}: {
+ aiResponse: string;
+ sources: string[];
+}) {
+ return (
+ <div
+ style={{
+ backgroundImage: `linear-gradient(to right, #E5D9F2, #CDC1FF)`,
+ }}
+ className="w-full max-w-2xl mx-auto px-4 py-6 space-y-6 border mt-4 rounded-xl"
+ >
+ <div className="text-start">
+ <div className="text-xl text-black">
+ <Markdown remarkPlugins={[remarkGfm]}>{aiResponse.replace('</s>', '')}</Markdown>
+ </div>
+ </div>
+ <div className="grid gap-6">
+ {sources.map((value, index) => (
+ <Card key={index}>
+ <CardContent className="space-y-2">{value}</CardContent>
+ </Card>
+ ))}
+ </div>
+ </div>
+ );
+}
+
+export default SearchResults;