mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
Merge branch 'new-ui' of https://github.com/Dhravya/supermemory into new-ui
This commit is contained in:
commit
f4917ef8f6
6 changed files with 104 additions and 219 deletions
|
|
@ -31,22 +31,25 @@ export async function POST(req: NextRequest) {
|
|||
chatHistory: ChatHistory[]
|
||||
};
|
||||
|
||||
console.log("CHathistory", chatHistory)
|
||||
|
||||
if (!query) {
|
||||
return new Response(JSON.stringify({ message: "Invalid query" }), { status: 400 });
|
||||
}
|
||||
|
||||
|
||||
const resp = await fetch(`https://cf-ai-backend.dhravya.workers.dev/chat?q=${query}&user=${session.user.email ?? session.user.name}&sourcesOnly=${sourcesOnly}`, {
|
||||
headers: {
|
||||
"X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
chatHistory
|
||||
chatHistory: chatHistory.chatHistory ?? []
|
||||
})
|
||||
})
|
||||
|
||||
console.log(resp.status)
|
||||
console.log(resp.statusText)
|
||||
|
||||
if (resp.status !== 200 || !resp.ok) {
|
||||
const errorData = await resp.json();
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ import Image from 'next/image';
|
|||
function ChatMessage({
|
||||
message,
|
||||
user,
|
||||
sources,
|
||||
}: {
|
||||
message: string;
|
||||
user: User | 'ai';
|
||||
sources?: string[];
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import { MemoryDrawer } from "./MemoryDrawer";
|
|||
import useViewport from "@/hooks/useViewport";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
import SearchResults from "./SearchResults";
|
||||
import { ChatHistory } from "../../types/memory";
|
||||
import { ChatMessage } from "./ChatMessage";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Card, CardContent } from "./ui/card";
|
||||
|
||||
function supportsDVH() {
|
||||
try {
|
||||
|
|
@ -21,8 +21,6 @@ function supportsDVH() {
|
|||
}
|
||||
|
||||
export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
||||
return <Chat sidebarOpen={sidebarOpen} />;
|
||||
|
||||
const [hide, setHide] = useState(false);
|
||||
const [value, setValue] = useState("");
|
||||
const { width } = useViewport();
|
||||
|
|
@ -37,7 +35,7 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
// TEMPORARY solution: Basically this is to just keep track of the sources used for each chat message
|
||||
// Not a great solution
|
||||
const [chatTextSourceDict, setChatTextSourceDict] = useState<
|
||||
Record<string, string>
|
||||
Record<string, string[]>
|
||||
>({});
|
||||
|
||||
// helper function to append a new msg
|
||||
|
|
@ -107,7 +105,36 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
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);
|
||||
// setAIResponse((prev) => prev + parsedPart.response);
|
||||
// appendToChatHistory('model', parsedPart.response);
|
||||
|
||||
// Append to chat history in this way:
|
||||
// If the last message was from the model, append to that message
|
||||
// Otherwise, Start a new message from the model and append to that
|
||||
if (
|
||||
chatHistory.length > 0 &&
|
||||
chatHistory[chatHistory.length - 1].role === "model"
|
||||
) {
|
||||
setChatHistory((prev: any) => {
|
||||
const lastMessage = prev[prev.length - 1];
|
||||
const newParts = [
|
||||
...lastMessage.parts,
|
||||
{ text: parsedPart.response },
|
||||
];
|
||||
return [
|
||||
...prev.slice(0, prev.length - 1),
|
||||
{ ...lastMessage, parts: newParts },
|
||||
];
|
||||
});
|
||||
} else {
|
||||
setChatHistory((prev) => [
|
||||
...prev,
|
||||
{
|
||||
role: "model",
|
||||
parts: [{ text: parsedPart.response }],
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If parsing fails and it's not the last part, it's a malformed JSON
|
||||
|
|
@ -139,17 +166,35 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
e.preventDefault();
|
||||
setIsAiLoading(true);
|
||||
|
||||
console.log(value);
|
||||
|
||||
appendToChatHistory("user", value);
|
||||
|
||||
const sourcesResponse = await fetch(
|
||||
`/api/query?sourcesOnly=true&q=${value}`,
|
||||
`/api/chat?sourcesOnly=true&q=${value}`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
chatHistory,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const sourcesInJson = (await sourcesResponse.json()) as {
|
||||
ids: string[];
|
||||
};
|
||||
|
||||
setSearchResults(sourcesInJson.ids);
|
||||
setSearchResults((prev) =>
|
||||
Array.from(new Set([...prev, ...sourcesInJson.ids])),
|
||||
);
|
||||
|
||||
const response = await fetch(`/api/query?q=${value}`);
|
||||
// TODO: PASS THE `SPACE` TO THE API
|
||||
const response = await fetch(`/api/chat?q=${value}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
chatHistory,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
setIsAiLoading(false);
|
||||
|
|
@ -164,8 +209,10 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
// @ts-ignore
|
||||
reader.read().then(function processText({ done, value }) {
|
||||
if (done) {
|
||||
// setSearchResults(JSON.parse(result.replace('data: ', '')));
|
||||
// setIsAiLoading(false);
|
||||
setIsAiLoading(false);
|
||||
setToBeParsed("");
|
||||
setValue("");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -177,7 +224,7 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
};
|
||||
|
||||
return (
|
||||
<main
|
||||
<motion.main
|
||||
data-sidebar-open={sidebarOpen}
|
||||
ref={main}
|
||||
className={cn(
|
||||
|
|
@ -189,43 +236,55 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
{chatHistory.map((chat, index) => (
|
||||
<ChatMessage
|
||||
key={index}
|
||||
message={chat.parts[0].text}
|
||||
message={chat.parts.map((part) => part.text).join("")}
|
||||
user={chat.role === "model" ? "ai" : session?.user!}
|
||||
/>
|
||||
))}
|
||||
{searchResults.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h1>Related memories</h1>
|
||||
<div className="grid gap-6">
|
||||
{searchResults.map((value, index) => (
|
||||
<Card key={index}>
|
||||
<CardContent className="space-y-2">{value}</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-rgray-11 mt-auto w-full text-center text-3xl md:mt-0">
|
||||
Ask your Second brain
|
||||
</h1>
|
||||
|
||||
<Textarea2
|
||||
ref={textArea}
|
||||
className="mt-auto h-max max-h-[30em] min-h-[3em] resize-y flex-row items-start justify-center overflow-auto py-5 md:mt-0 md:h-[20vh] md:resize-none md:flex-col md:items-center md:justify-center md:p-2 md:pb-2 md:pt-2"
|
||||
textAreaProps={{
|
||||
placeholder: "Ask your SuperMemory...",
|
||||
className:
|
||||
"h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-2 md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]",
|
||||
value,
|
||||
autoFocus: true,
|
||||
onChange: (e) => setValue(e.target.value),
|
||||
}}
|
||||
<form
|
||||
className="overflow-none mt-auto h-max min-h-[3em] w-full resize-y flex-row items-start justify-center py-5 md:mt-0 md:h-[20vh] md:resize-none md:flex-col md:items-center md:justify-center md:p-2 md:pb-2 md:pt-2"
|
||||
onSubmit={async (e) => await getSearchResults(e)}
|
||||
>
|
||||
<div className="text-rgray-11/70 flex h-full w-fit items-center justify-center pl-0 md:w-full md:p-2">
|
||||
<FilterCombobox className="hidden md:flex" />
|
||||
<button
|
||||
type="submit"
|
||||
disabled={value.trim().length < 1}
|
||||
className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:ml-auto md:mt-0"
|
||||
>
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</Textarea2>
|
||||
{/* {searchResults && (
|
||||
<SearchResults aiResponse={aiResponse} sources={searchResults} />
|
||||
)} */}
|
||||
<Textarea2
|
||||
ref={textArea}
|
||||
textAreaProps={{
|
||||
placeholder: "Ask your SuperMemory...",
|
||||
className:
|
||||
"h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-2 pt-2 md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]",
|
||||
value,
|
||||
autoFocus: true,
|
||||
onChange: (e) => setValue(e.target.value),
|
||||
}}
|
||||
>
|
||||
<div className="text-rgray-11/70 flex h-full w-fit items-center justify-center pl-0 md:w-full md:p-2">
|
||||
<FilterCombobox className="hidden md:flex" />
|
||||
<button
|
||||
type="submit"
|
||||
disabled={value.trim().length < 1}
|
||||
className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:ml-auto md:mt-0"
|
||||
>
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</Textarea2>
|
||||
</form>
|
||||
{width <= 768 && <MemoryDrawer hide={hide} />}
|
||||
</main>
|
||||
</motion.main>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,139 +0,0 @@
|
|||
'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;
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
'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;
|
||||
|
|
@ -31,8 +31,6 @@ export const MemoryProvider: React.FC<
|
|||
[spaces],
|
||||
);
|
||||
|
||||
console.log(spaces);
|
||||
|
||||
return (
|
||||
<MemoryContext.Provider value={{ spaces, addSpace, deleteSpace }}>
|
||||
{children}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue