Merge pull request #79 from Dhravya/chathistory

addeed chathistory functionality
This commit is contained in:
Dhravya Shah 2024-06-22 20:38:00 -05:00 committed by GitHub
commit 47e7528f67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1009 additions and 628 deletions

View file

@ -1,3 +1,3 @@
.tlui-dialog__overlay {
position: fixed;
position: fixed;
}

View file

@ -12,43 +12,40 @@ import { getAssetUrls } from "@tldraw/assets/selfHosted";
import { memo } from "react";
import DragContext from "./lib/context";
import DropZone from "./dropComponent";
import './canvas.css'
import "./canvas.css";
export const Canvas = memo(() => {
const [isDraggingOver, setIsDraggingOver] = useState<boolean>(false);
const Dragref = useRef<HTMLDivElement | null>(null)
const Dragref = useRef<HTMLDivElement | null>(null);
const handleDragOver = (event: any) => {
event.preventDefault();
setIsDraggingOver(true);
console.log("entere")
console.log("entere");
};
useEffect(() => {
const divElement = Dragref.current;
if (divElement) {
divElement.addEventListener('dragover', handleDragOver);
divElement.addEventListener("dragover", handleDragOver);
}
return () => {
if (divElement) {
divElement.removeEventListener('dragover', handleDragOver);
divElement.removeEventListener("dragover", handleDragOver);
}
};
}, []);
return (
<DragContext.Provider value={{ isDraggingOver, setIsDraggingOver }}>
<div
ref={Dragref}
className="w-full h-full"
>
<TldrawComponent />
</div>
<div ref={Dragref} className="w-full h-full">
<TldrawComponent />
</div>
</DragContext.Provider>
);
});
const TldrawComponent =memo(() => {
const TldrawComponent = memo(() => {
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({
status: "loading",
});
@ -94,4 +91,4 @@ const TldrawComponent =memo(() => {
</Tldraw>
</div>
);
})
});

View file

@ -52,14 +52,14 @@ function DropZone() {
console.log("leaver");
};
useEffect(()=> {
setInterval(()=> {
useEffect(() => {
setInterval(() => {
editor.selectAll();
const shapes = editor.getSelectedShapes();
const text = shapes.filter((s) => s.type === "text")
console.log("hrhh", text)
},5000)
}, [])
const text = shapes.filter((s) => s.type === "text");
console.log("hrhh", text);
}, 5000);
}, []);
const handleDrop = useCallback((event: DragEvent) => {
event.preventDefault();
@ -111,19 +111,23 @@ function DropZone() {
className={`h-full flex justify-center items-center w-full absolute top-0 left-0 z-[100000] pointer-events-none ${isDraggingOver && "bg-[#2c3439ad] pointer-events-auto"}`}
ref={dropRef}
>
{
isDraggingOver&& (
<>
<div className="absolute top-4 left-8"><TopRight /></div>
<div className="absolute top-4 right-8"><TopLeft /></div>
<div className="absolute bottom-4 left-8"><BottomLeft /></div>
<div className="absolute bottom-4 right-8"><BottomRight /></div>
{isDraggingOver && (
<>
<div className="absolute top-4 left-8">
<TopRight />
</div>
<div className="absolute top-4 right-8">
<TopLeft />
</div>
<div className="absolute bottom-4 left-8">
<BottomLeft />
</div>
<div className="absolute bottom-4 right-8">
<BottomRight />
</div>
<h2 className="text-2xl">Drop here to add Content on Canvas</h2>
</>
)
}
</>
)}
</div>
);
}

View file

@ -1,17 +1,34 @@
import { AssetRecordType, Editor, TLAsset, TLAssetId, TLBookmarkShape, TLExternalContentSource, TLShapePartial, Vec, VecLike, createShapeId, getEmbedInfo, getHashForString } from "tldraw";
export default async function createEmbedsFromUrl({url, point, sources, editor}: {
url: string
point?: VecLike | undefined
sources?: TLExternalContentSource[] | undefined
editor: Editor
}){
import {
AssetRecordType,
Editor,
TLAsset,
TLAssetId,
TLBookmarkShape,
TLExternalContentSource,
TLShapePartial,
Vec,
VecLike,
createShapeId,
getEmbedInfo,
getHashForString,
} from "tldraw";
export default async function createEmbedsFromUrl({
url,
point,
sources,
editor,
}: {
url: string;
point?: VecLike | undefined;
sources?: TLExternalContentSource[] | undefined;
editor: Editor;
}) {
const position =
point ??
(editor.inputs.shiftKey
? editor.inputs.currentPagePoint
: editor.getViewportPageBounds().center);
point ??
(editor.inputs.shiftKey
? editor.inputs.currentPagePoint
: editor.getViewportPageBounds().center);
if (url?.includes("x.com") || url?.includes("twitter.com")) {
return editor.createShape({
@ -20,71 +37,71 @@ export default async function createEmbedsFromUrl({url, point, sources, editor}:
y: position.y - 150,
props: { url: url },
});
}
// try to paste as an embed first
const embedInfo = getEmbedInfo(url);
// try to paste as an embed first
const embedInfo = getEmbedInfo(url);
if (embedInfo) {
return editor.putExternalContent({
type: "embed",
url: embedInfo.url,
point,
embed: embedInfo.definition,
});
}
const assetId: TLAssetId = AssetRecordType.createId(
getHashForString(url),
);
const shape = createEmptyBookmarkShape(editor, url, position);
// Use an existing asset if we have one, or else else create a new one
let asset = editor.getAsset(assetId) as TLAsset;
let shouldAlsoCreateAsset = false;
if (!asset) {
shouldAlsoCreateAsset = true;
try {
const bookmarkAsset = await editor.getAssetForExternalContent({
type: "url",
url,
});
const fetchWebsite: {
title?: string;
image?: string;
description?: string;
} = await (await fetch(`/api/unfirlsite?website=${url}`, {
method: "POST"
})).json()
if (bookmarkAsset){
if (fetchWebsite.title) bookmarkAsset.props.title = fetchWebsite.title;
if (fetchWebsite.image) bookmarkAsset.props.image = fetchWebsite.image;
if (fetchWebsite.description) bookmarkAsset.props.description = fetchWebsite.description;
}
if (!bookmarkAsset) throw Error("Could not create an asset");
asset = bookmarkAsset;
} catch (e) {
console.log(e)
return;
}
}
editor.batch(() => {
if (shouldAlsoCreateAsset) {
editor.createAssets([asset]);
}
editor.updateShapes([
{
id: shape.id,
type: shape.type,
props: {
assetId: asset.id,
},
},
]);
if (embedInfo) {
return editor.putExternalContent({
type: "embed",
url: embedInfo.url,
point,
embed: embedInfo.definition,
});
}
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url));
const shape = createEmptyBookmarkShape(editor, url, position);
// Use an existing asset if we have one, or else else create a new one
let asset = editor.getAsset(assetId) as TLAsset;
let shouldAlsoCreateAsset = false;
if (!asset) {
shouldAlsoCreateAsset = true;
try {
const bookmarkAsset = await editor.getAssetForExternalContent({
type: "url",
url,
});
const fetchWebsite: {
title?: string;
image?: string;
description?: string;
} = await (
await fetch(`/api/unfirlsite?website=${url}`, {
method: "POST",
})
).json();
if (bookmarkAsset) {
if (fetchWebsite.title) bookmarkAsset.props.title = fetchWebsite.title;
if (fetchWebsite.image) bookmarkAsset.props.image = fetchWebsite.image;
if (fetchWebsite.description)
bookmarkAsset.props.description = fetchWebsite.description;
}
if (!bookmarkAsset) throw Error("Could not create an asset");
asset = bookmarkAsset;
} catch (e) {
console.log(e);
return;
}
}
editor.batch(() => {
if (shouldAlsoCreateAsset) {
editor.createAssets([asset]);
}
editor.updateShapes([
{
id: shape.id,
type: shape.type,
props: {
assetId: asset.id,
},
},
]);
});
}
function isURL(str: string) {
@ -115,17 +132,23 @@ function formatTextToRatio(text: string) {
if (currentLine) {
lines.push(currentLine);
}
return {height: (lines.length+1)*18, width: maxLineWidth*10};
return { height: (lines.length + 1) * 18, width: maxLineWidth * 10 };
}
export function handleExternalDroppedContent({text, editor}: {text:string, editor: Editor}){
export function handleExternalDroppedContent({
text,
editor,
}: {
text: string;
editor: Editor;
}) {
const position = editor.inputs.shiftKey
? editor.inputs.currentPagePoint
: editor.getViewportPageBounds().center;
? editor.inputs.currentPagePoint
: editor.getViewportPageBounds().center;
if (isURL(text)){
createEmbedsFromUrl({editor, url: text})
} else{
if (isURL(text)) {
createEmbedsFromUrl({ editor, url: text });
} else {
// editor.createShape({
// type: "text",
// x: position.x - 75,
@ -136,66 +159,76 @@ export function handleExternalDroppedContent({text, editor}: {text:string, edito
// textAlign: "start",
// },
// });
const {height, width} =formatTextToRatio(text)
const { height, width } = formatTextToRatio(text);
editor.createShape({
type: "Textcard",
x: position.x - (width/2),
y: position.y - (height/2),
props: { content:text, extrainfo: "https://chatgpt.com/c/762cd44e-1752-495b-967a-aa3c23c6024a", w: width, h:height },
x: position.x - width / 2,
y: position.y - height / 2,
props: {
content: text,
extrainfo: "https://chatgpt.com/c/762cd44e-1752-495b-967a-aa3c23c6024a",
w: width,
h: height,
},
});
}
}
function centerSelectionAroundPoint(editor: Editor, position: VecLike) {
// Re-position shapes so that the center of the group is at the provided point
const viewportPageBounds = editor.getViewportPageBounds()
let selectionPageBounds = editor.getSelectionPageBounds()
// Re-position shapes so that the center of the group is at the provided point
const viewportPageBounds = editor.getViewportPageBounds();
let selectionPageBounds = editor.getSelectionPageBounds();
if (selectionPageBounds) {
const offset = selectionPageBounds!.center.sub(position)
if (selectionPageBounds) {
const offset = selectionPageBounds!.center.sub(position);
editor.updateShapes(
editor.getSelectedShapes().map((shape) => {
const localRotation = editor.getShapeParentTransform(shape).decompose().rotation
const localDelta = Vec.Rot(offset, -localRotation)
return {
id: shape.id,
type: shape.type,
x: shape.x! - localDelta.x,
y: shape.y! - localDelta.y,
}
})
)
}
editor.updateShapes(
editor.getSelectedShapes().map((shape) => {
const localRotation = editor
.getShapeParentTransform(shape)
.decompose().rotation;
const localDelta = Vec.Rot(offset, -localRotation);
return {
id: shape.id,
type: shape.type,
x: shape.x! - localDelta.x,
y: shape.y! - localDelta.y,
};
}),
);
}
// Zoom out to fit the shapes, if necessary
selectionPageBounds = editor.getSelectionPageBounds()
if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) {
editor.zoomToSelection()
}
// Zoom out to fit the shapes, if necessary
selectionPageBounds = editor.getSelectionPageBounds();
if (
selectionPageBounds &&
!viewportPageBounds.contains(selectionPageBounds)
) {
editor.zoomToSelection();
}
}
export function createEmptyBookmarkShape(
editor: Editor,
url: string,
position: VecLike
editor: Editor,
url: string,
position: VecLike,
): TLBookmarkShape {
const partial: TLShapePartial = {
id: createShapeId(),
type: 'bookmark',
x: position.x - 150,
y: position.y - 160,
opacity: 1,
props: {
assetId: null,
url,
},
}
const partial: TLShapePartial = {
id: createShapeId(),
type: "bookmark",
x: position.x - 150,
y: position.y - 160,
opacity: 1,
props: {
assetId: null,
url,
},
};
editor.batch(() => {
editor.createShapes([partial]).select(partial.id)
centerSelectionAroundPoint(editor, position)
})
editor.batch(() => {
editor.createShapes([partial]).select(partial.id);
centerSelectionAroundPoint(editor, position);
});
return editor.getShape(partial.id) as TLBookmarkShape
}
return editor.getShape(partial.id) as TLBookmarkShape;
}

View file

@ -1,6 +1,6 @@
import { createTLStore, defaultShapeUtils, loadSnapshot } from "tldraw";
import { twitterCardUtil } from "../twitterCard";
import {textCardUtil} from "../textCard"
import { textCardUtil } from "../textCard";
export async function loadRemoteSnapshot() {
const res = await fetch(
"https://learning-cf.pruthvirajthinks.workers.dev/get/page3",
@ -9,6 +9,6 @@ export async function loadRemoteSnapshot() {
const newStore = createTLStore({
shapeUtils: [...defaultShapeUtils, twitterCardUtil, textCardUtil],
});
loadSnapshot(newStore, snapshot)
loadSnapshot(newStore, snapshot);
return newStore;
}
}

View file

@ -1,4 +1,11 @@
import { BaseBoxShapeUtil, HTMLContainer, TLBaseBoxShape, TLBaseShape, useIsEditing, useValue } from "tldraw";
import {
BaseBoxShapeUtil,
HTMLContainer,
TLBaseBoxShape,
TLBaseShape,
useIsEditing,
useValue,
} from "tldraw";
type ITextCardShape = TLBaseShape<
"Textcard",
@ -18,26 +25,29 @@ export class textCardUtil extends BaseBoxShapeUtil<ITextCardShape> {
}
component(s: ITextCardShape) {
const isEditing = useIsEditing(s.id)
const isEditing = useIsEditing(s.id);
const isHoveringWhileEditingSameShape = useValue(
'is hovering',
"is hovering",
() => {
const { editingShapeId, hoveredShapeId } = this.editor.getCurrentPageState()
const { editingShapeId, hoveredShapeId } =
this.editor.getCurrentPageState();
if (editingShapeId && hoveredShapeId !== editingShapeId) {
const editingShape = this.editor.getShape(editingShapeId)
if (editingShape && this.editor.isShapeOfType<TLBaseBoxShape>(editingShape, 'embed')) {
return true
const editingShape = this.editor.getShape(editingShapeId);
if (
editingShape &&
this.editor.isShapeOfType<TLBaseBoxShape>(editingShape, "embed")
) {
return true;
}
}
return false
},
[]
)
const isInteractive = isEditing || isHoveringWhileEditingSameShape
return false;
},
[],
);
const isInteractive = isEditing || isHoveringWhileEditingSameShape;
return (
<HTMLContainer className="flex h-full w-full items-center justify-center">
<div
@ -48,7 +58,7 @@ export class textCardUtil extends BaseBoxShapeUtil<ITextCardShape> {
zIndex: isInteractive ? "" : "-1",
background: "#2C3439",
borderRadius: "16px",
padding: "8px 14px"
padding: "8px 14px",
}}
>
<h1 style={{ fontSize: "15px" }}>{s.props.content}</h1>

View file

@ -0,0 +1,38 @@
import { getFullChatThread } from "@/app/actions/fetchers";
import { chatSearchParamsCache } from "@/lib/searchParams";
import ChatWindow from "../chatWindow";
async function Page({
params,
searchParams,
}: {
params: { chatid: string };
searchParams: Record<string, string | string[] | undefined>;
}) {
const { firstTime, q, spaces } = chatSearchParamsCache.parse(searchParams);
let chat: Awaited<ReturnType<typeof getFullChatThread>>;
try {
chat = await getFullChatThread(params.chatid);
} catch (e) {
const error = e as Error;
return <div>This page errored out: {error.message}</div>;
}
if (!chat.success || !chat.data) {
console.error(chat.error);
return <div>Chat not found. Check the console for more details.</div>;
}
return (
<ChatWindow
q={q}
spaces={spaces}
initialChat={chat.data.length > 0 ? chat.data : undefined}
threadId={params.chatid}
/>
);
}
export default Page;

View file

@ -23,16 +23,18 @@ import { codeLanguageSubset } from "@/lib/constants";
import { z } from "zod";
import { toast } from "sonner";
import Link from "next/link";
import { createChatObject } from "@/app/actions/doers";
import {
ClipboardIcon,
ShareIcon,
SpeakerWaveIcon,
} from "@heroicons/react/24/outline";
import { SendIcon } from "lucide-react";
function ChatWindow({
q,
spaces,
}: {
q: string;
spaces: { id: string; name: string }[];
}) {
const [layout, setLayout] = useState<"chat" | "initial">("initial");
const [chatHistory, setChatHistory] = useState<ChatHistory[]>([
initialChat = [
{
question: q,
answer: {
@ -40,8 +42,18 @@ function ChatWindow({
sources: [],
},
},
]);
const [isAutoScroll, setIsAutoScroll] = useState(true);
],
threadId,
}: {
q: string;
spaces: { id: string; name: string }[];
initialChat?: ChatHistory[];
threadId: string;
}) {
const [layout, setLayout] = useState<"chat" | "initial">(
initialChat.length > 1 ? "chat" : "initial",
);
const [chatHistory, setChatHistory] = useState<ChatHistory[]>(initialChat);
const removeJustificationFromText = (text: string) => {
// remove everything after the first "<justification>" word
@ -61,7 +73,7 @@ function ChatWindow({
const getAnswer = async (query: string, spaces: string[]) => {
const sourcesFetch = await fetch(
`/api/chat?q=${query}&spaces=${spaces}&sourcesOnly=true`,
`/api/chat?q=${query}&spaces=${spaces}&sourcesOnly=true&threadId=${threadId}`,
{
method: "POST",
body: JSON.stringify({ chatHistory }),
@ -79,79 +91,108 @@ function ChatWindow({
const sourcesParsed = sourcesZod.safeParse(sources);
if (!sourcesParsed.success) {
console.log(sources);
console.error(sourcesParsed.error);
toast.error("Something went wrong while getting the sources");
return;
}
window.scrollTo({
top: document.documentElement.scrollHeight,
behavior: "smooth",
});
setChatHistory((prevChatHistory) => {
window.scrollTo({
top: document.documentElement.scrollHeight,
behavior: "smooth",
// Assuming this is part of a larger function within a React component
const updateChatHistoryAndFetch = async () => {
// Step 1: Update chat history with the assistant's response
await new Promise((resolve) => {
setChatHistory((prevChatHistory) => {
const newChatHistory = [...prevChatHistory];
const lastAnswer = newChatHistory[newChatHistory.length - 1];
if (!lastAnswer) {
resolve(undefined);
return prevChatHistory;
}
const filteredSourceUrls = new Set(
sourcesParsed.data.metadata.map((source) => source.url),
);
const uniqueSources = sourcesParsed.data.metadata.filter((source) => {
if (filteredSourceUrls.has(source.url)) {
filteredSourceUrls.delete(source.url);
return true;
}
return false;
});
lastAnswer.answer.sources = uniqueSources.map((source) => ({
title: source.title ?? "Untitled",
type: source.type ?? "page",
source: source.url ?? "https://supermemory.ai",
content: source.description ?? "No content available",
numChunks: sourcesParsed.data.metadata.filter(
(f) => f.url === source.url,
).length,
}));
resolve(newChatHistory);
return newChatHistory;
});
});
const newChatHistory = [...prevChatHistory];
const lastAnswer = newChatHistory[newChatHistory.length - 1];
if (!lastAnswer) return prevChatHistory;
const filteredSourceUrls = new Set(
sourcesParsed.data.metadata.map((source) => source.url),
// Step 2: Fetch data from the API
const resp = await fetch(
`/api/chat?q=${query}&spaces=${spaces}&threadId=${threadId}`,
{
method: "POST",
body: JSON.stringify({ chatHistory }),
},
);
const uniqueSources = sourcesParsed.data.metadata.filter((source) => {
if (filteredSourceUrls.has(source.url)) {
filteredSourceUrls.delete(source.url);
return true;
// Step 3: Read the response stream and update the chat history
const reader = resp.body?.getReader();
let done = false;
while (!done && reader) {
const { value, done: d } = await reader.read();
if (d) {
setChatHistory((prevChatHistory) => {
createChatObject(threadId, prevChatHistory);
return prevChatHistory;
});
}
return false;
});
lastAnswer.answer.sources = uniqueSources.map((source) => ({
title: source.title ?? "Untitled",
type: source.type ?? "page",
source: source.url ?? "https://supermemory.ai",
content: source.description ?? "No content available",
numChunks: sourcesParsed.data.metadata.filter(
(f) => f.url === source.url,
).length,
}));
return newChatHistory;
});
done = d;
const resp = await fetch(`/api/chat?q=${query}&spaces=${spaces}`, {
method: "POST",
body: JSON.stringify({ chatHistory }),
});
const reader = resp.body?.getReader();
let done = false;
while (!done && reader) {
const { value, done: d } = await reader.read();
done = d;
setChatHistory((prevChatHistory) => {
const newChatHistory = [...prevChatHistory];
const lastAnswer = newChatHistory[newChatHistory.length - 1];
if (!lastAnswer) return prevChatHistory;
const txt = new TextDecoder().decode(value);
setChatHistory((prevChatHistory) => {
const newChatHistory = [...prevChatHistory];
const lastAnswer = newChatHistory[newChatHistory.length - 1];
if (!lastAnswer) return prevChatHistory;
if (isAutoScroll) {
window.scrollTo({
top: document.documentElement.scrollHeight,
behavior: "smooth",
});
}
lastAnswer.answer.parts.push({ text: txt });
return newChatHistory;
});
}
lastAnswer.answer.parts.push({ text: txt });
return newChatHistory;
});
}
};
updateChatHistoryAndFetch();
};
useEffect(() => {
if (q.trim().length > 0) {
if (q.trim().length > 0 || chatHistory.length > 0) {
setLayout("chat");
getAnswer(
q,
spaces.map((s) => s.id),
);
const lastChat = chatHistory.length > 0 ? chatHistory.length - 1 : 0;
const startGenerating = chatHistory[lastChat]?.answer.parts[0]?.text
? false
: true;
if (startGenerating) {
getAnswer(
q,
spaces.map((s) => `${s}`),
);
}
} else {
router.push("/home");
}
@ -177,150 +218,207 @@ function ChatWindow({
</motion.div>
) : (
<div
className="max-w-3xl relative flex mx-auto w-full flex-col mt-24 pb-32"
className="max-w-3xl z-10 mx-auto relative h-full overflow-y-auto no-scrollbar"
key="chat"
>
{chatHistory.map((chat, idx) => (
<div
key={idx}
className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b border-b-gray-400" : ""}`}
>
<h2
className={cn(
"text-white transition-all transform translate-y-0 opacity-100 duration-500 ease-in-out font-semibold text-2xl",
)}
>
{chat.question}
</h2>
<div className="flex flex-col gap-2 mt-2">
<div className="w-full pt-24 mb-40">
{chatHistory.map((chat, idx) => (
<div key={idx} className="space-y-16">
<div
className={`${chat.answer.sources.length > 0 || chat.answer.parts.length === 0 ? "flex" : "hidden"}`}
className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b border-b-gray-400" : ""}`}
>
<Accordion
defaultValue={
idx === chatHistory.length - 1 ? "memories" : ""
}
type="single"
collapsible
>
<AccordionItem value="memories">
<AccordionTrigger className="text-foreground-menu">
Related Memories
</AccordionTrigger>
{/* TODO: fade out content on the right side, the fade goes away when the user scrolls */}
<AccordionContent
className="relative flex gap-2 max-w-3xl overflow-auto no-scrollbar"
defaultChecked
>
{/* Loading state */}
{chat.answer.sources.length > 0 ||
(chat.answer.parts.length === 0 && (
<>
{[1, 2, 3, 4].map((_, idx) => (
<div
key={`loadingState-${idx}`}
className="rounded-xl bg-secondary p-4 flex flex-col gap-2 min-w-72 animate-pulse"
>
<div className="bg-slate-700 h-2 rounded-full w-1/2"></div>
<div className="bg-slate-700 h-2 rounded-full w-full"></div>
</div>
))}
</>
))}
{chat.answer.sources.map((source, idx) => (
<Link
href={source.source}
key={idx}
className="rounded-xl bg-secondary p-4 flex flex-col gap-2 min-w-72"
>
<div className="flex justify-between text-foreground-menu text-sm">
<span>{source.type}</span>
{source.numChunks > 1 && (
<span>{source.numChunks} chunks</span>
)}
</div>
<div className="text-base">{source.title}</div>
<div className="text-xs">
{source.content.length > 100
? source.content.slice(0, 100) + "..."
: source.content}
</div>
</Link>
))}
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
{/* Summary */}
<div>
<div className="text-foreground-menu py-2">Summary</div>
<div className="text-base">
{chat.answer.parts.length === 0 && (
<div className="animate-pulse flex space-x-4">
<div className="flex-1 space-y-3 py-1">
<div className="h-2 bg-slate-700 rounded"></div>
<div className="h-2 bg-slate-700 rounded"></div>
</div>
</div>
<h2
className={cn(
"text-white transition-all transform translate-y-0 opacity-100 duration-500 ease-in-out font-semibold text-xl",
)}
<Markdown
remarkPlugins={[remarkGfm, [remarkMath]]}
rehypePlugins={[
rehypeKatex,
[
rehypeHighlight,
{
detect: true,
ignoreMissing: true,
subset: codeLanguageSubset,
},
],
]}
components={{
code: code as any,
p: p as any,
}}
className="flex flex-col gap-2"
>
{removeJustificationFromText(
chat.answer.parts.map((part) => part.text).join(""),
)}
</Markdown>
</div>
</div>
{/* Justification */}
{chat.answer.justification &&
chat.answer.justification.length && (
>
{chat.question}
</h2>
<div className="flex flex-col">
{/* Related memories */}
<div
className={`${chat.answer.justification && chat.answer.justification.length > 0 ? "flex" : "hidden"}`}
className={`space-y-4 ${chat.answer.sources.length > 0 || chat.answer.parts.length === 0 ? "flex" : "hidden"}`}
>
<Accordion defaultValue={""} type="single" collapsible>
<AccordionItem value="justification">
<Accordion
defaultValue={
idx === chatHistory.length - 1 ? "memories" : ""
}
type="single"
collapsible
>
<AccordionItem value="memories">
<AccordionTrigger className="text-foreground-menu">
Justification
Related Memories
</AccordionTrigger>
{/* TODO: fade out content on the right side, the fade goes away when the user scrolls */}
<AccordionContent
className="relative flex gap-2 max-w-3xl overflow-auto no-scrollbar"
className="flex items-center no-scrollbar overflow-auto gap-4 relative max-w-3xl no-scrollbar"
defaultChecked
>
{chat.answer.justification.length > 0
? chat.answer.justification
.replaceAll("<justification>", "")
.replaceAll("</justification>", "")
: "No justification provided."}
{/* Loading state */}
{chat.answer.sources.length > 0 ||
(chat.answer.parts.length === 0 && (
<>
{[1, 2, 3, 4].map((_, idx) => (
<div
key={`loadingState-${idx}`}
className="w-[350px] shrink-0 p-4 gap-2 rounded-2xl flex flex-col bg-secondary animate-pulse"
>
<div className="bg-slate-700 h-2 rounded-full w-1/2"></div>
<div className="bg-slate-700 h-2 rounded-full w-full"></div>
</div>
))}
</>
))}
{chat.answer.sources.map((source, idx) => (
<Link
href={source.source}
key={idx}
className="w-[350px] shrink-0 p-4 gap-2 rounded-2xl flex flex-col bg-secondary"
>
<div className="flex justify-between text-foreground-menu text-sm">
<span>{source.type}</span>
{source.numChunks > 1 && (
<span>{source.numChunks} chunks</span>
)}
</div>
<div className="text-base">
{source.title}
</div>
<div className="text-xs line-clamp-2">
{source.content.length > 100
? source.content.slice(0, 100) + "..."
: source.content}
</div>
</Link>
))}
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
)}
</div>
</div>
))}
<div className="fixed bottom-0 w-full max-w-3xl pb-4">
{/* Summary */}
<div>
<div className="text-foreground-menu py-2">Summary</div>
<div className="text-base">
{/* Loading state */}
{(chat.answer.parts.length === 0 ||
chat.answer.parts.join("").length === 0) && (
<div className="animate-pulse flex space-x-4">
<div className="flex-1 space-y-3 py-1">
<div className="h-2 bg-slate-700 rounded"></div>
<div className="h-2 bg-slate-700 rounded"></div>
</div>
</div>
)}
<Markdown
remarkPlugins={[remarkGfm, [remarkMath]]}
rehypePlugins={[
rehypeKatex,
[
rehypeHighlight,
{
detect: true,
ignoreMissing: true,
subset: codeLanguageSubset,
},
],
]}
components={{
code: code as any,
p: p as any,
}}
className="flex flex-col gap-2 text-base"
>
{removeJustificationFromText(
chat.answer.parts
.map((part) => part.text)
.join(""),
)}
</Markdown>
<div className="mt-3 relative -left-2 flex items-center gap-1">
{/* TODO: speak response */}
{/* <button className="group h-8 w-8 flex justify-center items-center active:scale-75 duration-200">
<SpeakerWaveIcon className="size-[18px] group-hover:text-primary" />
</button> */}
{/* copy response */}
<button
onClick={() =>
navigator.clipboard.writeText(
chat.answer.parts
.map((part) => part.text)
.join(""),
)
}
className="group h-8 w-8 flex justify-center items-center active:scale-75 duration-200"
>
<ClipboardIcon className="size-[18px] group-hover:text-primary" />
</button>
<button
onClick={async () => {
const isWebShareSupported =
navigator.share !== undefined;
if (isWebShareSupported) {
try {
await navigator.share({
title: "Your Share Title",
text: "Your share text or description",
url: "https://your-url-to-share.com",
});
} catch (e) {
console.error("Error sharing:", e);
}
} else {
console.error("web share is not supported!");
}
}}
className="group h-8 w-8 flex justify-center items-center active:scale-75 duration-200"
>
<SendIcon className="size-[18px] group-hover:text-primary" />
</button>
</div>
</div>
</div>
{/* Justification */}
{chat.answer.justification &&
chat.answer.justification.length && (
<div
className={`${chat.answer.justification && chat.answer.justification.length > 0 ? "flex" : "hidden"}`}
>
<Accordion
defaultValue={""}
type="single"
collapsible
>
<AccordionItem value="justification">
<AccordionTrigger className="text-foreground-menu">
Justification
</AccordionTrigger>
<AccordionContent
className="relative flex gap-2 max-w-3xl overflow-auto no-scrollbar"
defaultChecked
>
{chat.answer.justification.length > 0
? chat.answer.justification
.replaceAll("<justification>", "")
.replaceAll("</justification>", "")
: "No justification provided."}
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
)}
</div>
</div>
</div>
))}
</div>
<div className="fixed bottom-4 w-full max-w-3xl">
<QueryInput
mini
className="w-full shadow-md"

View file

@ -1,108 +0,0 @@
import ChatWindow from "./chatWindow";
import { chatSearchParamsCache } from "../../helpers/lib/searchParams";
import { ChevronDownIcon, ClipboardIcon, SpeakerWaveIcon } from '@heroicons/react/24/outline'
import Image from "next/image";
import { ArrowRightIcon } from "@repo/ui/icons";
import QueryInput from "@repo/ui/components/QueryInput";
// @ts-expect-error
await import("katex/dist/katex.min.css");
function Page({
searchParams,
}: {
searchParams: Record<string, string | string[] | undefined>;
}) {
const { firstTime, q, spaces } = chatSearchParamsCache.parse(searchParams);
console.log(spaces);
return (
<div className="max-w-3xl z-10 mx-auto relative h-full overflow-y-auto no-scrollbar">
{/* <ChatWindow q={q} spaces={[]} /> */}
<div className="w-full pt-24 space-y-40">
{/* single q&A */}
{Array.from({ length: 1 }).map((_, i) => (
<div key={i} className="space-y-16">
{/* header */}
<div>
{/* query */}
<h1 className="text-white text-xl">Why is Retrieval-Augmented Generation important?</h1>
</div>
{/* response */}
<div className="space-y-10">
{/* related memories */}
<div className="space-y-4">
{/* section header */}
<div className="flex items-center gap-3">
<h1>Related memories</h1>
<button>
<ChevronDownIcon className="size-4 stroke-2" />
</button>
</div>
{/* section content */}
{/* collection of memories */}
<div className="flex items-center no-scrollbar overflow-auto gap-4">
{/* related memory */}
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="w-[350px] shrink-0 p-4 gap-2 rounded-2xl flex flex-col bg-secondary">
<h3 className="text-[13px]">Webpage</h3>
<p className="line-clamp-2 text-white">What is RAG? - Retrieval-Augmented Generation Explained - AWS</p>
</div>
))}
</div>
</div>
{/* summary */}
<div className="space-y-4">
{/* section header */}
<div className="flex items-center gap-3">
<h1>Summary</h1>
<button>
<ChevronDownIcon className="size-4 stroke-2" />
</button>
</div>
{/* section content */}
<div>
<p className="text-white text-base">
Retrieval-Augmented Generation is crucial because it combines the strengths of retrieval-based methods, ensuring relevance and accuracy, with generation-based models, enabling creativity and flexibility. By integrating retrieval mechanisms, it addresses data sparsity issues, improves content relevance, offers fine-tuned control over output, handles ambiguity, and allows for continual learning, making it highly adaptable and effective across various natural language processing tasks and domains.
</p>
{/* response actions */}
<div className="mt-3 relative -left-2 flex items-center gap-1">
{/* speak response */}
<button className="group h-8 w-8 flex justify-center items-center active:scale-75 duration-200">
<SpeakerWaveIcon className="size-[18px] group-hover:text-primary" />
</button>
{/* copy response */}
<button className="group h-8 w-8 flex justify-center items-center active:scale-75 duration-200">
<ClipboardIcon className="size-[18px] group-hover:text-primary" />
</button>
</div>
</div>
</div>
</div>
</div>
))}
</div>
<div className="fixed bottom-4 max-w-3xl w-full">
<QueryInput />
</div>
</div>
);
}
export default Page;

View file

@ -71,16 +71,13 @@ function DynamicIslandContent() {
}
const lastBtn = useRef<string>();
useEffect(() => {
console.log(show);
}, [show]);
useEffect(() => {
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
setshow(true);
}
console.log(e.key, lastBtn.current);
if (e.key === "a" && lastBtn.current === "Alt") {
setshow(false);
}
@ -90,19 +87,15 @@ function DynamicIslandContent() {
return (
<>
{show ? (
<div
<button
onClick={() => setshow(!show)}
className="bg-secondary px-3 w-[2.23rem] overflow-hidden hover:w-[9.2rem] whitespace-nowrap py-2 rounded-3xl transition-[width] cursor-pointer"
className="bg-secondary p-2 text-[#989EA4] rounded-full flex items-center justify-between gap-2 px-4 h-10 pr-5 z-[999] shadow-md"
>
<div className="flex gap-4 items-center">
<Image src={AddIcon} alt="Add icon" />
Add Content
</div>
</div>
<Image src={AddIcon} alt="add icon" />
Add content
</button>
) : (
<div>
<ToolBar cancelfn={cancelfn} />
</div>
<ToolBar cancelfn={cancelfn} />
)}
</>
);
@ -272,7 +265,6 @@ function PageForm({
spaces: space ? [space] : undefined,
});
console.log(cont);
setLoading(false);
if (cont.success) {
toast.success("Memory created");

View file

@ -9,7 +9,6 @@ import DynamicIsland from "./dynamicisland";
function Header() {
return (
<div className="p-4 relative z-30 h-16 flex items-center">
<div className="w-full flex items-center justify-between">
<Link className="" href="/home">
<Image
@ -20,14 +19,7 @@ function Header() {
</Link>
<div className="fixed z-30 left-1/2 -translate-x-1/2 top-5">
{/* <DynamicIsland /> */}
<button className="bg-secondary p-2 text-[#989EA4] rounded-full flex items-center justify-between gap-2 px-4 h-10 pr-5">
<Image
src={AddIcon}
alt="add icon"
/>
Add content
</button>
<DynamicIsland />
</div>
<button className="flex duration-200 items-center text-[#7D8994] hover:bg-[#1F2429] text-[13px] gap-2 px-3 py-2 rounded-xl">
@ -35,7 +27,6 @@ function Header() {
Start new chat
</button>
</div>
</div>
);
}

View file

@ -5,6 +5,7 @@ import QueryInput from "./queryinput";
import { homeSearchParamsCache } from "@/lib/searchParams";
import { getSpaces } from "@/app/actions/fetchers";
import { useRouter } from "next/navigation";
import { createChatThread } from "@/app/actions/doers";
function Page({
searchParams,
@ -12,7 +13,8 @@ function Page({
searchParams: Record<string, string | string[] | undefined>;
}) {
// TODO: use this to show a welcome page/modal
const { firstTime } = homeSearchParamsCache.parse(searchParams);
// const { firstTime } = homeSearchParamsCache.parse(searchParams);
const { push } = useRouter();
const [spaces, setSpaces] = useState<{ id: number; name: string }[]>([]);
@ -20,13 +22,12 @@ function Page({
getSpaces().then((res) => {
if (res.success && res.data) {
setSpaces(res.data);
return;
}
// TODO: HANDLE ERROR
});
}, []);
const { push } = useRouter();
return (
<div className="max-w-3xl h-full justify-center flex mx-auto w-full flex-col">
{/* all content goes here */}
@ -34,13 +35,12 @@ function Page({
<div className="w-full pb-20">
<QueryInput
handleSubmit={(q, spaces) => {
const newQ =
"/chat?q=" +
encodeURI(q) +
(spaces ? "&spaces=" + JSON.stringify(spaces) : "");
handleSubmit={async (q, spaces) => {
const threadid = await createChatThread(q);
push(newQ);
push(
`/chat/${threadid.data}?spaces=${JSON.stringify(spaces)}&q=${q}`,
);
}}
initialSpaces={spaces}
/>

View file

@ -69,7 +69,7 @@ function QueryInput({
name="q"
cols={30}
rows={mini ? 2 : 4}
className="bg-transparent pt-2.5 text-base placeholder:text-[#5D6165] text-[#9DA0A4] focus:text-white duration-200 tracking-[3%] outline-none resize-none w-full p-4"
className="bg-transparent pt-2.5 text-base placeholder:text-[#5D6165] text-[#9DA0A4] focus:text-gray-200 duration-200 tracking-[3%] outline-none resize-none w-full p-4"
placeholder="Ask your second brain..."
onKeyDown={(e) => {
if (e.key === "Enter") {
@ -85,7 +85,7 @@ function QueryInput({
<button
type="submit"
onClick={e => e.preventDefault()}
onClick={(e) => e.preventDefault()}
disabled={disabled}
className="h-12 w-12 rounded-[14px] bg-[#21303D] all-center shrink-0 hover:brightness-125 duration-200 outline-none focus:outline focus:outline-primary active:scale-90"
>

View file

@ -13,16 +13,13 @@ async function Layout({ children }: { children: React.ReactNode }) {
return (
<main className="h-screen flex flex-col">
<div className="fixed top-0 left-0 w-full">
<Header />
</div>
<Menu />
<div className="w-full h-full">
{children}
</div>
<div className="w-full h-full">{children}</div>
<Toaster />
</main>

View file

@ -1,6 +1,11 @@
import React from "react";
import Image from "next/image";
import { MemoriesIcon, ExploreIcon, HistoryIcon, CanvasIcon } from "@repo/ui/icons";
import {
MemoriesIcon,
ExploreIcon,
HistoryIcon,
CanvasIcon,
} from "@repo/ui/icons";
import Link from "next/link";
function Menu() {

View file

@ -2,7 +2,13 @@
import { revalidatePath } from "next/cache";
import { db } from "../../server/db";
import { contentToSpace, space, storedContent } from "../../server/db/schema";
import {
chatHistory,
chatThreads,
contentToSpace,
space,
storedContent,
} from "../../server/db/schema";
import { ServerActionReturnType } from "./types";
import { auth } from "../../server/auth";
import { Tweet } from "react-tweet/api";
@ -10,6 +16,7 @@ import { getMetaData } from "@/lib/get-metadata";
import { and, eq, inArray, sql } from "drizzle-orm";
import { LIMITS } from "@/lib/constants";
import { z } from "zod";
import { ChatHistory } from "@repo/shared-types";
export const createSpace = async (
input: string | FormData,
@ -266,3 +273,74 @@ export const createMemory = async (input: {
};
}
};
export const createChatThread = async (
firstMessage: string,
): ServerActionReturnType<string> => {
const data = await auth();
if (!data || !data.user || !data.user.id) {
return { error: "Not authenticated", success: false };
}
const thread = await db
.insert(chatThreads)
.values({
firstMessage,
userId: data.user.id,
})
.returning({ id: chatThreads.id })
.execute();
console.log(thread);
if (!thread[0]) {
return {
success: false,
error: "Failed to create chat thread",
};
}
return { success: true, data: thread[0].id };
};
export const createChatObject = async (
threadId: string,
chatHistorySoFar: ChatHistory[],
): ServerActionReturnType<boolean> => {
const data = await auth();
if (!data || !data.user || !data.user.id) {
return { error: "Not authenticated", success: false };
}
const lastChat = chatHistorySoFar[chatHistorySoFar.length - 1];
if (!lastChat) {
return {
success: false,
data: false,
error: "No chat object found",
};
}
console.log("sources: ", lastChat.answer.sources);
const saved = await db.insert(chatHistory).values({
question: lastChat.question,
answer: lastChat.answer.parts.map((part) => part.text).join(""),
answerSources: JSON.stringify(lastChat.answer.sources),
threadId,
});
if (!saved) {
return {
success: false,
data: false,
error: "Failed to save chat object",
};
}
return {
success: true,
data: true,
};
};

View file

@ -1,8 +1,10 @@
"use server";
import { eq, inArray, not, sql } from "drizzle-orm";
import { and, asc, eq, inArray, not, sql } from "drizzle-orm";
import { db } from "../../server/db";
import {
chatHistory,
chatThreads,
Content,
contentToSpace,
storedContent,
@ -10,6 +12,8 @@ import {
} from "../../server/db/schema";
import { ServerActionReturnType, Space } from "./types";
import { auth } from "../../server/auth";
import { ChatHistory, SourceZod } from "@repo/shared-types";
import { z } from "zod";
export const getSpaces = async (): ServerActionReturnType<Space[]> => {
const data = await auth();
@ -103,22 +107,27 @@ export const getAllUserMemoriesAndSpaces = async (): ServerActionReturnType<{
// console.log(contentCountBySpace);
// get a count with space mappings like spaceID: count (number of memories in that space)
const contentCountBySpace = await db
.select({
spaceId: contentToSpace.spaceId,
count: sql<number>`count(*)`.mapWith(Number),
})
.from(contentToSpace)
.where(
inArray(
contentToSpace.spaceId,
spacesWithoutUser.map((space) => space.id),
),
)
.groupBy(contentToSpace.spaceId)
.execute();
console.log(contentCountBySpace);
const len = spacesWithoutUser.map((space) => space.id).length;
if (len > 0) {
const contentCountBySpace = await db
.select({
spaceId: contentToSpace.spaceId,
count: sql<number>`count(*)`.mapWith(Number),
})
.from(contentToSpace)
.where(
inArray(
contentToSpace.spaceId,
spacesWithoutUser.map((space) => space.id),
),
)
.groupBy(contentToSpace.spaceId)
.execute();
console.log(contentCountBySpace);
}
const contentNotInAnySpace = await db
.select()
@ -140,3 +149,64 @@ export const getAllUserMemoriesAndSpaces = async (): ServerActionReturnType<{
data: { spaces: spacesWithoutUser, memories: contentNotInAnySpace },
};
};
export const getFullChatThread = async (
threadId: string,
): ServerActionReturnType<ChatHistory[]> => {
const data = await auth();
if (!data || !data.user || !data.user.id) {
return { error: "Not authenticated", success: false };
}
const thread = await db.query.chatThreads.findFirst({
where: and(
eq(chatThreads.id, threadId),
eq(chatThreads.userId, data.user.id),
),
});
if (!thread) {
return { error: "Thread not found", success: false };
}
const allChatsInThisThread = await db.query.chatHistory
.findMany({
where: and(eq(chatHistory.threadId, threadId)),
orderBy: asc(chatHistory.id),
})
.execute();
const accumulatedChatHistory: ChatHistory[] = allChatsInThisThread.map(
(chat) => {
console.log("answer sources", chat.answerSources);
const sourceCheck = z
.array(SourceZod)
.safeParse(JSON.parse(chat.answerSources ?? "[]"));
if (!sourceCheck.success || !sourceCheck.data) {
console.error("sourceCheck.error", sourceCheck.error);
throw new Error("Invalid source data");
}
const sources = sourceCheck.data;
return {
question: chat.question,
answer: {
parts: [
{
text: chat.answer ?? undefined,
},
],
sources: sources ?? [],
},
};
},
);
return {
success: true,
data: accumulatedChatHistory,
};
};

View file

@ -1,9 +1,5 @@
import { type NextRequest } from "next/server";
import {
ChatHistory,
ChatHistoryZod,
convertChatHistoryList,
} from "@repo/shared-types";
import { ChatHistoryZod, convertChatHistoryList } from "@repo/shared-types";
import { ensureAuth } from "../ensureAuth";
import { z } from "zod";
@ -67,11 +63,8 @@ export async function POST(req: NextRequest) {
},
);
console.log("sourcesOnly", sourcesOnly);
if (sourcesOnly == "true") {
const data = await resp.json();
console.log("data", data);
return new Response(JSON.stringify(data), { status: 200 });
}

View file

@ -1,7 +1,7 @@
import { type Config } from "drizzle-kit";
export default {
schema: "./app/helpers/server/db/schema.ts",
schema: "./server/db/schema.ts",
dialect: "sqlite",
driver: "d1",
dbCredentials: {

View file

@ -16,11 +16,19 @@ export const chatSearchParamsCache = createSearchParamsCache({
firstTime: parseAsBoolean.withDefault(false),
q: parseAsString.withDefault(""),
spaces: parseAsArrayOf(
parseAsJson(() =>
z.object({
id: z.string(),
name: z.string(),
}),
),
parseAsJson((c) => {
const valid = z
.object({
id: z.string(),
name: z.string(),
})
.safeParse(c);
if (!valid.success) {
return null;
}
return valid.data;
}),
).withDefault([]),
});

View file

@ -27,6 +27,23 @@ CREATE TABLE `authenticator` (
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `chatHistory` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`threadId` text NOT NULL,
`question` text NOT NULL,
`answerParts` text,
`answerSources` text,
`answerJustification` text,
FOREIGN KEY (`threadId`) REFERENCES `chatThread`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `chatThread` (
`id` text PRIMARY KEY NOT NULL,
`firstMessage` text NOT NULL,
`userId` text NOT NULL,
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `contentToSpace` (
`contentId` integer NOT NULL,
`spaceId` integer NOT NULL,
@ -60,7 +77,7 @@ CREATE TABLE `storedContent` (
`ogImage` text(255),
`type` text DEFAULT 'page',
`image` text(255),
`user` integer,
`user` text,
FOREIGN KEY (`user`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
@ -80,6 +97,8 @@ CREATE TABLE `verificationToken` (
);
--> statement-breakpoint
CREATE UNIQUE INDEX `authenticator_credentialID_unique` ON `authenticator` (`credentialID`);--> statement-breakpoint
CREATE INDEX `chatHistory_thread_idx` ON `chatHistory` (`threadId`);--> statement-breakpoint
CREATE INDEX `chatThread_user_idx` ON `chatThread` (`userId`);--> statement-breakpoint
CREATE UNIQUE INDEX `space_name_unique` ON `space` (`name`);--> statement-breakpoint
CREATE INDEX `spaces_name_idx` ON `space` (`name`);--> statement-breakpoint
CREATE INDEX `spaces_user_idx` ON `space` (`user`);--> statement-breakpoint

View file

@ -1,7 +1,7 @@
{
"version": "6",
"dialect": "sqlite",
"id": "4a568d9b-a0e6-44ed-946b-694e34b063f3",
"id": "349eea0d-f26e-4579-9c65-3982816b0c6c",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"account": {
@ -191,6 +191,119 @@
},
"uniqueConstraints": {}
},
"chatHistory": {
"name": "chatHistory",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"threadId": {
"name": "threadId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"question": {
"name": "question",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"answerParts": {
"name": "answerParts",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"answerSources": {
"name": "answerSources",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"answerJustification": {
"name": "answerJustification",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"chatHistory_thread_idx": {
"name": "chatHistory_thread_idx",
"columns": ["threadId"],
"isUnique": false
}
},
"foreignKeys": {
"chatHistory_threadId_chatThread_id_fk": {
"name": "chatHistory_threadId_chatThread_id_fk",
"tableFrom": "chatHistory",
"tableTo": "chatThread",
"columnsFrom": ["threadId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"chatThread": {
"name": "chatThread",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"firstMessage": {
"name": "firstMessage",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"chatThread_user_idx": {
"name": "chatThread_user_idx",
"columns": ["userId"],
"isUnique": false
}
},
"foreignKeys": {
"chatThread_userId_user_id_fk": {
"name": "chatThread_userId_user_id_fk",
"tableFrom": "chatThread",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"contentToSpace": {
"name": "contentToSpace",
"columns": {
@ -411,7 +524,7 @@
},
"user": {
"name": "user",
"type": "integer",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false

View file

@ -5,8 +5,8 @@
{
"idx": 0,
"version": "6",
"when": 1718412145023,
"tag": "0000_absurd_pandemic",
"when": 1719075265633,
"tag": "0000_conscious_arachne",
"breakpoints": true
}
]

View file

@ -154,3 +154,40 @@ export type StoredSpace = typeof space.$inferSelect;
export type ChachedSpaceContent = StoredContent & {
space: number;
};
export const chatThreads = createTable(
"chatThread",
{
id: text("id")
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
firstMessage: text("firstMessage").notNull(),
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
},
(thread) => ({
userIdx: index("chatThread_user_idx").on(thread.userId),
}),
);
export const chatHistory = createTable(
"chatHistory",
{
id: integer("id").notNull().primaryKey({ autoIncrement: true }),
threadId: text("threadId")
.notNull()
.references(() => chatThreads.id, { onDelete: "cascade" }),
question: text("question").notNull(),
answer: text("answerParts"), // Single answer part as string
answerSources: text("answerSources"), // JSON stringified array of objects
answerJustification: text("answerJustification"),
},
(history) => ({
threadIdx: index("chatHistory_thread_idx").on(history.threadId),
}),
);
export type ChatThread = typeof chatThreads.$inferSelect;
export type ChatHistory = typeof chatHistory.$inferSelect;

View file

@ -47,7 +47,7 @@
"@aws-sdk/s3-request-presigner": "^3.577.0",
"@cloudflare/puppeteer": "^0.0.11",
"@headlessui/react": "^2.0.4",
"@heroicons/react": "^2.1.3",
"@heroicons/react": "^2.1.4",
"@hono/swagger-ui": "^0.2.2",
"@hookform/resolvers": "^3.4.2",
"@iarna/toml": "^2.2.5",

View file

@ -1,18 +1,20 @@
import { z } from "zod";
export const SourceZod = z.object({
type: z.string(),
source: z.string(),
title: z.string(),
content: z.string(),
numChunks: z.number().optional().default(1),
});
export type Source = z.infer<typeof SourceZod>;
export const ChatHistoryZod = z.object({
question: z.string(),
answer: z.object({
parts: z.array(z.object({ text: z.string() })),
sources: z.array(
z.object({
type: z.enum(["note", "page", "tweet"]),
source: z.string(),
title: z.string(),
content: z.string(),
numChunks: z.number().optional().default(1),
}),
),
parts: z.array(z.object({ text: z.string().optional() })),
sources: z.array(SourceZod),
justification: z.string().optional(),
}),
});
@ -52,5 +54,8 @@ export function convertChatHistoryList(
);
});
// THE LAST ASSISTANT CONTENT WILL ALWAYS BE EMPTY, so we remove it
convertedChats.pop();
return convertedChats;
}

View file

@ -211,12 +211,13 @@ body {
width: 0px;
}
::-moz-selection { /* Code for Firefox */
color: #369DFD;
background: #21303D;
::-moz-selection {
/* Code for Firefox */
color: #369dfd;
background: #21303d;
}
::selection {
color: #369DFD;
background: #21303D;
}
color: #369dfd;
background: #21303d;
}

View file

@ -1,46 +1,46 @@
import React from 'react'
import Divider from '../shadcn/divider'
import { ArrowRightIcon } from '../icons'
import Image from 'next/image'
import React from "react";
import Divider from "../shadcn/divider";
import { ArrowRightIcon } from "../icons";
import Image from "next/image";
function QueryInput() {
return (
<div>
<div className="bg-secondary rounded-[20px] h-[68 px]">
{/* input and action button */}
<form className="flex gap-4 p-2.5">
<textarea
name="q"
cols={30}
rows={4}
className="bg-transparent h-12 focus:h-[128px] no-scrollbar pt-3 px-2 text-base placeholder:text-[#5D6165] text-[#9DA0A4] focus:text-white duration-200 tracking-[3%] outline-none resize-none w-full"
placeholder="Ask your second brain..."
// onKeyDown={(e) => {
// if (e.key === "Enter") {
// e.preventDefault();
// if (!e.shiftKey) push(parseQ());
// }
// }}
// onChange={(e) => setQ(e.target.value)}
// value={q}
// disabled={disabled}
/>
return (
<div>
<div className="bg-secondary rounded-[20px] h-[68 px]">
{/* input and action button */}
<form className="flex gap-4 p-2.5">
<textarea
name="q"
cols={30}
rows={4}
className="bg-transparent h-12 focus:h-[128px] no-scrollbar pt-3 px-2 text-base placeholder:text-[#5D6165] text-[#9DA0A4] focus:text-white duration-200 tracking-[3%] outline-none resize-none w-full"
placeholder="Ask your second brain..."
// onKeyDown={(e) => {
// if (e.key === "Enter") {
// e.preventDefault();
// if (!e.shiftKey) push(parseQ());
// }
// }}
// onChange={(e) => setQ(e.target.value)}
// value={q}
// disabled={disabled}
/>
<button
// type="submit"
// onClick={e => e.preventDefault()}
// disabled={disabled}
className="h-12 w-12 rounded-[14px] bg-[#21303D] all-center shrink-0 hover:brightness-125 duration-200 outline-none focus:outline focus:outline-primary active:scale-90"
>
<Image src={ArrowRightIcon} alt="Right arrow icon" />
</button>
</form>
<button
// type="submit"
// onClick={e => e.preventDefault()}
// disabled={disabled}
className="h-12 w-12 rounded-[14px] bg-[#21303D] all-center shrink-0 hover:brightness-125 duration-200 outline-none focus:outline focus:outline-primary active:scale-90"
>
<Image src={ArrowRightIcon} alt="Right arrow icon" />
</button>
</form>
{/* <Divider /> */}
</div>
{/* selected sources */}
{/* <div className="flex items-center gap-6 p-2 h-auto bg-secondary"> */}
{/* <MultipleSelector
{/* <Divider /> */}
</div>
{/* selected sources */}
{/* <div className="flex items-center gap-6 p-2 h-auto bg-secondary"> */}
{/* <MultipleSelector
key={options.length}
disabled={disabled}
defaultOptions={options}
@ -52,9 +52,9 @@ function QueryInput() {
</p>
}
/> */}
{/* </div> */}
</div>
)
{/* </div> */}
</div>
);
}
export default QueryInput
export default QueryInput;

View file

@ -51,14 +51,13 @@ function DraggableComponents({
setIsDragging(false);
};
return (
<div
ref={containerRef}
onDragEnd={handleDragEnd}
onDragStart={handleDragStart}
draggable
className={`flex gap-4 px-1 rounded-md text-[#989EA4] border-2 transition ${isDragging ? "border-blue-600": "border-[#1F2428]"}`}
className={`flex gap-4 px-1 rounded-md text-[#989EA4] border-2 transition ${isDragging ? "border-blue-600" : "border-[#1F2428]"}`}
>
<Image className="select-none" src={icon} alt={iconAlt} />
<div className="flex flex-col gap-2">

View file

@ -8,7 +8,7 @@ import SelectIcon from "./select.svg";
import SearchIcon from "./search.svg";
import NextIcon from "./nextarrow.svg";
import UrlIcon from "./url.svg";
import CanvasIcon from "./canvas.svg";
import CanvasIcon from "./canvas.svg";
import blockIcon from "./block.svg";
import LinkIcon from "./link.svg";
import AutocompleteIcon from "./autocomplete.svg";
@ -33,5 +33,5 @@ export {
AutocompleteIcon,
BlockIcon,
DragIcon,
SettingsIcon
SettingsIcon,
};

View file

@ -2,7 +2,7 @@
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { cn } from "@repo/ui/lib/utils";
@ -30,7 +30,8 @@ const AccordionTrigger = React.forwardRef<
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
<ChevronDownIcon className="size-4 stroke-2 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));

View file

@ -1,7 +1,7 @@
"use client"
"use client";
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@repo/ui/lib/utils";
@ -12,18 +12,18 @@ const Switch = React.forwardRef<
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch }
export { Switch };