mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-05 23:40:57 +00:00
Merge pull request #79 from Dhravya/chathistory
addeed chathistory functionality
This commit is contained in:
commit
47e7528f67
33 changed files with 1009 additions and 628 deletions
|
|
@ -1,3 +1,3 @@
|
|||
.tlui-dialog__overlay {
|
||||
position: fixed;
|
||||
position: fixed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
38
apps/web/app/(dash)/chat/[chatid]/page.tsx
Normal file
38
apps/web/app/(dash)/chat/[chatid]/page.tsx
Normal 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;
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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([]),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1718412145023,
|
||||
"tag": "0000_absurd_pandemic",
|
||||
"when": 1719075265633,
|
||||
"tag": "0000_conscious_arachne",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue