diff --git a/apps/web/app/(canvas)/canvas.css b/apps/web/app/(canvas)/canvas.css new file mode 100644 index 00000000..3e6700da --- /dev/null +++ b/apps/web/app/(canvas)/canvas.css @@ -0,0 +1,3 @@ +.tlui-dialog__overlay { + position: fixed; +} diff --git a/apps/web/app/(canvas)/canvas.tsx b/apps/web/app/(canvas)/canvas.tsx index 498ab1eb..aaf89a06 100644 --- a/apps/web/app/(canvas)/canvas.tsx +++ b/apps/web/app/(canvas)/canvas.tsx @@ -4,6 +4,7 @@ import { createAssetFromUrl } from "./lib/createAssetUrl"; import "tldraw/tldraw.css"; import { components } from "./enabledComp"; import { twitterCardUtil } from "./twitterCard"; +import { textCardUtil } from "./textCard"; import createEmbedsFromUrl from "./lib/createEmbeds"; import { loadRemoteSnapshot } from "./lib/loadSnap"; import { SaveStatus } from "./savesnap"; @@ -11,49 +12,40 @@ import { getAssetUrls } from "@tldraw/assets/selfHosted"; import { memo } from "react"; import DragContext from "./lib/context"; import DropZone from "./dropComponent"; +import "./canvas.css"; export const Canvas = memo(() => { const [isDraggingOver, setIsDraggingOver] = useState(false); - const Dragref = useRef(null) + const Dragref = useRef(null); const handleDragOver = (event: any) => { event.preventDefault(); setIsDraggingOver(true); - console.log("entere") - }; - - const handleDragLeave = () => { - setIsDraggingOver(false); - console.log("leaver") + console.log("entere"); }; useEffect(() => { const divElement = Dragref.current; if (divElement) { - divElement.addEventListener('dragover', handleDragOver); - divElement.addEventListener('dragleave', handleDragLeave); + divElement.addEventListener("dragover", handleDragOver); } return () => { if (divElement) { - divElement.removeEventListener('dragover', handleDragOver); - divElement.removeEventListener('dragleave', handleDragLeave); + divElement.removeEventListener("dragover", handleDragOver); } }; }, []); return ( -
- -
+
+ +
); }); -const TldrawComponent =memo(() => { +const TldrawComponent = memo(() => { const [storeWithStatus, setStoreWithStatus] = useState({ status: "loading", }); @@ -89,7 +81,7 @@ const TldrawComponent =memo(() => { assetUrls={assetUrls} components={components} store={storeWithStatus} - shapeUtils={[twitterCardUtil]} + shapeUtils={[twitterCardUtil, textCardUtil]} onMount={handleMount} >
@@ -99,4 +91,4 @@ const TldrawComponent =memo(() => {
); -}) +}); diff --git a/apps/web/app/(canvas)/canvas/page.tsx b/apps/web/app/(canvas)/canvas/page.tsx index 366a4481..533ed33c 100644 --- a/apps/web/app/(canvas)/canvas/page.tsx +++ b/apps/web/app/(canvas)/canvas/page.tsx @@ -1,92 +1,70 @@ "use client"; -// import Canvas from "./_components/canvas"; -import {Canvas} from "../canvas"; +import { Canvas } from "../canvas"; import React, { useState } from "react"; -// import ReactTextareaAutosize from "react-textarea-autosize"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; -import { - DragSvg, - SettingsSvg, - LinkSvg, - ThreeDBlock, - TextLoadingSvg, -} from "../svg"; +import { SettingsIcon, DragIcon } from "@repo/ui/icons"; +import DraggableComponentsContainer from "@repo/ui/components/canvas/draggableComponent"; +import { AutocompleteIcon, blockIcon } from "@repo/ui/icons"; +import Image from "next/image"; +import { Switch } from "@repo/ui/shadcn/switch"; +import { Label } from "@repo/ui/shadcn/label"; function page() { - const [value, setValue] = useState(""); const [fullScreen, setFullScreen] = useState(false); + const [visible, setVisible] = useState(true); return ( -
+
- - {setTimeout(()=> setFullScreen(false), 50)}} onCollapse={()=> {setTimeout(()=> setFullScreen(true), 50)}} defaultSize={30} collapsible={true} minSize={22}> -
+ { + l[0]! < 20 ? setVisible(false) : setVisible(true); + }} + className={` ${fullScreen ? "w-[calc(100vw-2rem)]" : "w-screen"} transition-all`} + direction="horizontal" + > + { + setTimeout(() => setFullScreen(false), 50); + }} + onCollapse={() => { + setTimeout(() => setFullScreen(true), 50); + }} + defaultSize={30} + collapsible={true} + > +
Change Filters - -
-
- { - setValue(e.target.value); - }} - value={value} - // rows={1} - className="w-full resize-none rounded-xl bg-[#151515] px-3 py-4 text-xl text-[#989EA4] outline-none focus:outline-none sm:max-h-52" - /> -
-
-
- -

- Nvidia will most likely create monopoly in software industry - as they are already largest player in GPU hardware by 20... -

-
-
- -
-
-

- Nvidia currently dominates the GPU hardware market, with - a market share over 97%. This has led some to argue... -

-
-

- From space: GPU GOATS -

-
-
-
- -
-
-

- Nvidia currently dominates the GPU hardware market, with - a market share over 97%. This has led some to argue... -

-
-

- Page url: - https://www.cnbc.com/2024/05/23/nvidia-keeps-hitting-records-can-investors-still-buy-the-stock.html?&qsearchterm=nvidia -

-
-
+ setting-icon
+ {visible ? ( + + ) : ( +

+ Need more space to show! +

+ )}
- - {/*
*/} -
- + +
+ drag-icon
- {/*
*/} -
+
@@ -96,4 +74,52 @@ function page() { ); } +function SidePanel() { + const [value, setValue] = useState(""); + const [dragAsText, setDragAsText] = useState(false); + return ( + <> +
+ { + setValue(e.target.value); + }} + value={value} + // rows={1} + className="w-full resize-none rounded-xl bg-[#151515] px-3 py-4 text-xl text-[#989EA4] outline-none focus:outline-none sm:max-h-52" + /> +
+
+ setDragAsText(e)} + id="drag-text-mode" + /> + +
+ + + ); +} + export default page; + +const content = [ + { + content: + "Regional growth patterns diverge, with strong performance in the United States and several emerging markets, contrasted by weaker prospects in many advanced economies, particularly in Europe (World Economic Forum) (OECD). The rapid adoption of artificial intelligence (AI) is expected to drive productivity growth, especially in advanced economies, potentially mitigating labor shortages and boosting income levels in emerging markets (World Economic Forum) (OECD). However, ongoing geopolitical tensions and economic fragmentation are likely to maintain a level of uncertainty and volatility in the global economy (World Economic Forum.", + icon: AutocompleteIcon, + iconAlt: "Autocomplete", + extraInfo: + "Page Url: https://chatgpt.com/c/762cd44e-1752-495b-967a-aa3c23c6024a", + }, + { + content: + "As of mid-2024, the global economy is experiencing modest growth with significant regional disparities. Global GDP growth is projected to be around 3.1% in 2024, rising slightly to 3.2% in 2025. This performance, although below the pre-pandemic average, reflects resilience despite various economic pressures, including tight monetary conditions and geopolitical tensions (IMF)(OECD) Inflation is moderating faster than expected, with global headline inflation projected to fall to 5.8% in 2024 and 4.4% in 2025, contributing to improving real incomes and positive trade growth (IMF) (OECD)", + icon: blockIcon, + iconAlt: "Autocomplete", + extraInfo: + "Page Url: https://www.cnbc.com/2024/05/23/nvidia-keeps-hitting-records-can-investors-still-buy-the-stock.html?&qsearchterm=nvidia", + }, +]; diff --git a/apps/web/app/(canvas)/dropComponent.tsx b/apps/web/app/(canvas)/dropComponent.tsx index 03a32358..a14bd1f3 100644 --- a/apps/web/app/(canvas)/dropComponent.tsx +++ b/apps/web/app/(canvas)/dropComponent.tsx @@ -9,25 +9,65 @@ const stripHtmlTags = (html: string): string => { return div.textContent || div.innerText || ""; }; +function formatTextToRatio(text: string) { + const totalWidth = text.length; + const maxLineWidth = Math.floor(totalWidth / 4); + + const words = text.split(" "); + let lines = []; + let currentLine = ""; + + words.forEach((word) => { + // Check if adding the next word exceeds the maximum line width + if ((currentLine + word).length <= maxLineWidth) { + currentLine += (currentLine ? " " : "") + word; + } else { + // If the current line is full, push it to new line + lines.push(currentLine); + currentLine = word; + } + }); + if (currentLine) { + lines.push(currentLine); + } + return lines.join("\n"); +} + const useDrag = (): DragContextType => { const context = useContext(DragContext); if (!context) { - throw new Error('useCounter must be used within a CounterProvider'); + throw new Error("useCounter must be used within a CounterProvider"); } return context; }; - function DropZone() { const dropRef = useRef(null); - const {isDraggingOver, setIsDraggingOver} = useDrag(); + const { isDraggingOver, setIsDraggingOver } = useDrag(); const editor = useEditor(); - const handleDrop = useCallback((event: React.DragEvent) => { + const handleDragLeave = () => { + setIsDraggingOver(false); + console.log("leaver"); + }; + + useEffect(() => { + setInterval(() => { + editor.selectAll(); + const shapes = editor.getSelectedShapes(); + const text = shapes.filter((s) => s.type === "text"); + console.log("hrhh", text); + }, 5000); + }, []); + + const handleDrop = useCallback((event: DragEvent) => { event.preventDefault(); setIsDraggingOver(false); const dt = event.dataTransfer; + if (!dt) { + return; + } const items = dt.items; for (let i = 0; i < items.length; i++) { @@ -45,7 +85,8 @@ function DropZone() { } else if (items[i]!.kind === "string") { items[i]!.getAsString((data) => { const cleanText = stripHtmlTags(data); - handleExternalDroppedContent({editor,text:cleanText}) + const onethree = formatTextToRatio(cleanText); + handleExternalDroppedContent({ editor, text: onethree }); }); } } @@ -54,22 +95,116 @@ function DropZone() { useEffect(() => { const divElement = dropRef.current; if (divElement) { - // @ts-ignore divElement.addEventListener("drop", handleDrop); + divElement.addEventListener("dragleave", handleDragLeave); } return () => { if (divElement) { - // @ts-ignore divElement.removeEventListener("drop", handleDrop); + divElement.addEventListener("dragleave", handleDragLeave); } }; }, []); return (
+ > + {isDraggingOver && ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+

Drop here to add Content on Canvas

+ + )} +
+ ); +} + +function TopRight() { + return ( + + + + ); +} + +function TopLeft() { + return ( + + + + ); +} + +function BottomLeft() { + return ( + + + + ); +} + +function BottomRight() { + return ( + + + ); } diff --git a/apps/web/app/(canvas)/lib/createEmbeds.ts b/apps/web/app/(canvas)/lib/createEmbeds.ts index 0db3c71b..64eb0627 100644 --- a/apps/web/app/(canvas)/lib/createEmbeds.ts +++ b/apps/web/app/(canvas)/lib/createEmbeds.ts @@ -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) { @@ -96,79 +113,122 @@ function isURL(str: string) { } } +function formatTextToRatio(text: string) { + const totalWidth = text.length; + const maxLineWidth = Math.floor(totalWidth / 10); -export function handleExternalDroppedContent({text, editor}: {text:string, editor: Editor}){ + const words = text.split(" "); + let lines = []; + let currentLine = ""; + + words.forEach((word) => { + if ((currentLine + word).length <= maxLineWidth) { + currentLine += (currentLine ? " " : "") + word; + } else { + lines.push(currentLine); + currentLine = word; + } + }); + if (currentLine) { + lines.push(currentLine); + } + return { height: (lines.length + 1) * 18, width: maxLineWidth * 10 }; +} + +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, + // y: position.y - 75, + // props: { + // text: text, + // size: "s", + // textAlign: "start", + // }, + // }); + const { height, width } = formatTextToRatio(text); editor.createShape({ - type: "text", - x: position.x - 75, - y: position.y - 75, + type: "Textcard", + x: position.x - width / 2, + y: position.y - height / 2, props: { - text: text, - size: "s", - textAlign: "start", + 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 -} \ No newline at end of file + return editor.getShape(partial.id) as TLBookmarkShape; +} diff --git a/apps/web/app/(canvas)/lib/loadSnap.ts b/apps/web/app/(canvas)/lib/loadSnap.ts index 15aad998..c6b748a9 100644 --- a/apps/web/app/(canvas)/lib/loadSnap.ts +++ b/apps/web/app/(canvas)/lib/loadSnap.ts @@ -1,13 +1,14 @@ -import { createTLStore, defaultShapeUtils } from "tldraw"; +import { createTLStore, defaultShapeUtils, loadSnapshot } from "tldraw"; import { twitterCardUtil } from "../twitterCard"; +import { textCardUtil } from "../textCard"; export async function loadRemoteSnapshot() { const res = await fetch( "https://learning-cf.pruthvirajthinks.workers.dev/get/page3", ); const snapshot = JSON.parse(await res.json()); const newStore = createTLStore({ - shapeUtils: [...defaultShapeUtils, twitterCardUtil], + shapeUtils: [...defaultShapeUtils, twitterCardUtil, textCardUtil], }); - newStore.loadSnapshot(snapshot); + loadSnapshot(newStore, snapshot); return newStore; -} \ No newline at end of file +} diff --git a/apps/web/app/(canvas)/textCard.tsx b/apps/web/app/(canvas)/textCard.tsx new file mode 100644 index 00000000..065c5ae1 --- /dev/null +++ b/apps/web/app/(canvas)/textCard.tsx @@ -0,0 +1,76 @@ +import { + BaseBoxShapeUtil, + HTMLContainer, + TLBaseBoxShape, + TLBaseShape, + useIsEditing, + useValue, +} from "tldraw"; + +type ITextCardShape = TLBaseShape< + "Textcard", + { w: number; h: number; content: string; extrainfo: string } +>; + +export class textCardUtil extends BaseBoxShapeUtil { + static override type = "Textcard" as const; + + getDefaultProps(): ITextCardShape["props"] { + return { + w: 100, + h: 50, + content: "", + extrainfo: "", + }; + } + + component(s: ITextCardShape) { + const isEditing = useIsEditing(s.id); + const isHoveringWhileEditingSameShape = useValue( + "is hovering", + () => { + const { editingShapeId, hoveredShapeId } = + this.editor.getCurrentPageState(); + + if (editingShapeId && hoveredShapeId !== editingShapeId) { + const editingShape = this.editor.getShape(editingShapeId); + if ( + editingShape && + this.editor.isShapeOfType(editingShape, "embed") + ) { + return true; + } + } + + return false; + }, + [], + ); + + const isInteractive = isEditing || isHoveringWhileEditingSameShape; + return ( + +
+

{s.props.content}

+

+ {s.props.extrainfo} +

+
+
+ ); + } + + indicator(shape: ITextCardShape) { + return ; + } +} diff --git a/apps/web/app/(dash)/chat/chatWindow.tsx b/apps/web/app/(dash)/chat/chatWindow.tsx index 97530f60..2be1d6c1 100644 --- a/apps/web/app/(dash)/chat/chatWindow.tsx +++ b/apps/web/app/(dash)/chat/chatWindow.tsx @@ -24,6 +24,12 @@ 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, @@ -48,7 +54,6 @@ function ChatWindow({ initialChat.length > 1 ? "chat" : "initial", ); const [chatHistory, setChatHistory] = useState(initialChat); - const [isAutoScroll, setIsAutoScroll] = useState(true); const removeJustificationFromText = (text: string) => { // remove everything after the first "" word @@ -164,12 +169,10 @@ function ChatWindow({ const lastAnswer = newChatHistory[newChatHistory.length - 1]; if (!lastAnswer) return prevChatHistory; - if (isAutoScroll) { - window.scrollTo({ - top: document.documentElement.scrollHeight, - behavior: "smooth", - }); - } + window.scrollTo({ + top: document.documentElement.scrollHeight, + behavior: "smooth", + }); lastAnswer.answer.parts.push({ text: txt }); return newChatHistory; @@ -218,151 +221,207 @@ function ChatWindow({ ) : (
- {chatHistory.map((chat, idx) => ( -
-

- {chat.question} -

- -
+
+ {chatHistory.map((chat, idx) => ( +
0 || chat.answer.parts.length === 0 ? "flex" : "hidden"}`} + className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b border-b-gray-400" : ""}`} > - - - - Related Memories - - {/* TODO: fade out content on the right side, the fade goes away when the user scrolls */} - - {/* Loading state */} - {chat.answer.sources.length > 0 || - (chat.answer.parts.length === 0 && ( - <> - {[1, 2, 3, 4].map((_, idx) => ( -
-
-
-
- ))} - - ))} - {chat.answer.sources.map((source, idx) => ( - -
- {source.type} - - {source.numChunks > 1 && ( - {source.numChunks} chunks - )} -
-
{source.title}
-
- {source.content.length > 100 - ? source.content.slice(0, 100) + "..." - : source.content} -
- - ))} -
-
-
-
- - {/* Summary */} -
-
Summary
-
- {chat.answer.parts.length === 0 && ( -
-
-
-
-
-
+

- {removeJustificationFromText( - chat.answer.parts.map((part) => part.text).join(""), - )} - -

-
+ > + {chat.question} + - {/* Justification */} - {chat.answer.justification && - chat.answer.justification.length && ( +
+ {/* Related memories */}
0 ? "flex" : "hidden"}`} + className={`space-y-4 ${chat.answer.sources.length > 0 || chat.answer.parts.length === 0 ? "flex" : "hidden"}`} > - - + + - Justification + Related Memories + {/* TODO: fade out content on the right side, the fade goes away when the user scrolls */} - {chat.answer.justification.length > 0 - ? chat.answer.justification - .replaceAll("", "") - .replaceAll("", "") - : "No justification provided."} + {/* Loading state */} + {chat.answer.sources.length > 0 || + (chat.answer.parts.length === 0 && ( + <> + {[1, 2, 3, 4].map((_, idx) => ( +
+
+
+
+ ))} + + ))} + {chat.answer.sources.map((source, idx) => ( + +
+ {source.type} + + {source.numChunks > 1 && ( + {source.numChunks} chunks + )} +
+
+ {source.title} +
+
+ {source.content.length > 100 + ? source.content.slice(0, 100) + "..." + : source.content} +
+ + ))}
- )} -
-
- ))} -
+ {/* Summary */} +
+
Summary
+
+ {/* Loading state */} + {(chat.answer.parts.length === 0 || + chat.answer.parts.join("").length === 0) && ( +
+
+
+
+
+
+ )} + + + {removeJustificationFromText( + chat.answer.parts + .map((part) => part.text) + .join(""), + )} + + +
+ {/* TODO: speak response */} + {/* */} + {/* copy response */} + + +
+
+
+ {/* Justification */} + {chat.answer.justification && + chat.answer.justification.length && ( +
0 ? "flex" : "hidden"}`} + > + + + + Justification + + + {chat.answer.justification.length > 0 + ? chat.answer.justification + .replaceAll("", "") + .replaceAll("", "") + : "No justification provided."} + + + +
+ )} +
+
+
+ ))} +
+ +
; + return ( +
+ {/* */} + +
+ {/* single q&A */} + {Array.from({ length: 1 }).map((_, i) => ( +
+ {/* header */} +
+ {/* query */} +

+ Why is Retrieval-Augmented Generation important? +

+
+ + {/* response */} +
+ {/* related memories */} +
+ {/* section header */} +
+

Related memories

+ +
+ + {/* section content */} + {/* collection of memories */} +
+ {/* related memory */} + {Array.from({ length: 3 }).map((_, i) => ( +
+

Webpage

+

+ What is RAG? - Retrieval-Augmented Generation Explained + - AWS +

+
+ ))} +
+
+ + {/* summary */} +
+ {/* section header */} +
+

Summary

+ +
+ + {/* section content */} +
+

+ 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. +

+ + {/* response actions */} +
+ {/* speak response */} + + {/* copy response */} + +
+
+
+
+
+ ))} +
+ +
+ +
+
+ ); } export default Page; diff --git a/apps/web/app/(dash)/dynamicisland.tsx b/apps/web/app/(dash)/dynamicisland.tsx index 6fa56fae..6d3965ca 100644 --- a/apps/web/app/(dash)/dynamicisland.tsx +++ b/apps/web/app/(dash)/dynamicisland.tsx @@ -39,7 +39,7 @@ export function DynamicIsland() { }); return ( -
+
{show ? ( -
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" > -
- Add icon - Add Content -
-
+ add icon + Add content + ) : ( -
- -
+ )} ); diff --git a/apps/web/app/(dash)/header.tsx b/apps/web/app/(dash)/header.tsx index 026cb080..91c00125 100644 --- a/apps/web/app/(dash)/header.tsx +++ b/apps/web/app/(dash)/header.tsx @@ -2,26 +2,28 @@ import React from "react"; import Image from "next/image"; import Link from "next/link"; import Logo from "../../public/logo.svg"; -import { ChatIcon } from "@repo/ui/icons"; +import { AddIcon, ChatIcon } from "@repo/ui/icons"; import DynamicIsland from "./dynamicisland"; function Header() { return ( -
-
- +
+
+ SuperMemory logo - +
+ +
-
diff --git a/apps/web/app/(dash)/home/page.tsx b/apps/web/app/(dash)/home/page.tsx index 7226de24..b6cfd223 100644 --- a/apps/web/app/(dash)/home/page.tsx +++ b/apps/web/app/(dash)/home/page.tsx @@ -33,7 +33,7 @@ function Page({ {/* all content goes here */} {/*
hi {firstTime ? 'first time' : ''}
*/} -
+
{ const threadid = await createChatThread(q); diff --git a/apps/web/app/(dash)/home/queryinput.tsx b/apps/web/app/(dash)/home/queryinput.tsx index ce45e36b..46038225 100644 --- a/apps/web/app/(dash)/home/queryinput.tsx +++ b/apps/web/app/(dash)/home/queryinput.tsx @@ -69,7 +69,7 @@ function QueryInput({ name="q" cols={30} rows={mini ? 2 : 4} - className="bg-transparent pt-2.5 text-base text-[#989EA4] focus:text-foreground 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,6 +85,7 @@ function QueryInput({ - - - -
-
Background
- {HIGHLIGHT_COLORS.map(({ name, color }) => ( - { - editor.commands.unsetHighlight(); - name !== "Default" && editor.commands.setHighlight({ color }); - }} - className="flex cursor-pointer items-center justify-between px-2 py-1 text-sm hover:bg-[#21303D]" - > -
-
- A -
- {name} -
- {editor.isActive("highlight", { color }) && } -
- ))} -
-
- - ); -}; diff --git a/apps/web/app/(editor)/components/selectors/color-selector.tsx b/apps/web/app/(editor)/components/selectors/color-selector.tsx deleted file mode 100644 index 557c4255..00000000 --- a/apps/web/app/(editor)/components/selectors/color-selector.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { Check, ChevronDown } from "lucide-react"; -import { EditorBubbleItem, useEditor } from "novel"; - -import { Button } from "@repo/ui/shadcn/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@repo/ui/shadcn/popover"; -export interface BubbleColorMenuItem { - name: string; - color: string; -} - -const TEXT_COLORS: BubbleColorMenuItem[] = [ - { - name: "Default", - color: "var(--novel-black)", - }, - { - name: "Purple", - color: "#9333EA", - }, - { - name: "Red", - color: "#E00000", - }, - { - name: "Yellow", - color: "#EAB308", - }, - { - name: "Blue", - color: "#2563EB", - }, - { - name: "Green", - color: "#008A00", - }, - { - name: "Orange", - color: "#FFA500", - }, - { - name: "Pink", - color: "#BA4081", - }, - { - name: "Gray", - color: "#A8A29E", - }, -]; - - -interface ColorSelectorProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const ColorSelector = ({ open, onOpenChange }: ColorSelectorProps) => { - const { editor } = useEditor(); - - if (!editor) return null; - const activeColorItem = TEXT_COLORS.find(({ color }) => editor.isActive("textStyle", { color })); - - return ( - - - - - - -
-
Color
- {TEXT_COLORS.map(({ name, color }) => ( - { - editor.commands.unsetColor(); - name !== "Default" && - editor - .chain() - .focus() - .setColor(color || "") - .run(); - }} - className="flex cursor-pointer items-center justify-between px-2 py-1 text-sm hover:bg-accent" - > -
-
- A -
- {name} -
-
- ))} -
-
-
- ); -}; diff --git a/apps/web/app/(editor)/components/selectors/link-selector.tsx b/apps/web/app/(editor)/components/selectors/link-selector.tsx deleted file mode 100644 index 3dc28266..00000000 --- a/apps/web/app/(editor)/components/selectors/link-selector.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Button } from "@repo/ui/shadcn/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@repo/ui/shadcn/popover"; -import { cn } from "@repo/ui/lib/utils"; -import { Check, Trash } from "lucide-react"; -import { useEditor } from "novel"; -import { useEffect, useRef } from "react"; - -export function isValidUrl(url: string) { - try { - new URL(url); - return true; - } catch (_e) { - return false; - } -} -export function getUrlFromString(str: string) { - if (isValidUrl(str)) return str; - try { - if (str.includes(".") && !str.includes(" ")) { - return new URL(`https://${str}`).toString(); - } - } catch (_e) { - return null; - } -} -interface LinkSelectorProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const LinkSelector = ({ open, onOpenChange }: LinkSelectorProps) => { - const inputRef = useRef(null); - const { editor } = useEditor(); - - // Autofocus on input by default - useEffect(() => { - inputRef.current?.focus(); - }); - if (!editor) return null; - - return ( - - - - - -
{ - const target = e.currentTarget as HTMLFormElement; - e.preventDefault(); - const input = target[0] as HTMLInputElement; - const url = getUrlFromString(input.value); - url && editor.chain().focus().setLink({ href: url }).run(); - }} - className="flex p-1 " - > - - {editor.getAttributes("link").href ? ( - - ) : ( - - )} -
-
-
- ); -}; diff --git a/apps/web/app/(editor)/components/selectors/node-selector.tsx b/apps/web/app/(editor)/components/selectors/node-selector.tsx deleted file mode 100644 index c6092b68..00000000 --- a/apps/web/app/(editor)/components/selectors/node-selector.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { - Check, - CheckSquare, - ChevronDown, - Code, - Heading1, - Heading2, - Heading3, - ListOrdered, - type LucideIcon, - TextIcon, - TextQuote, -} from "lucide-react"; -import { EditorBubbleItem, useEditor } from "novel"; - -import { Button } from "@repo/ui/shadcn/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@repo/ui/shadcn/popover"; - -export type SelectorItem = { - name: string; - icon: LucideIcon; - command: (editor: ReturnType["editor"]) => void; - isActive: (editor: ReturnType["editor"]) => boolean; -}; - -const items: SelectorItem[] = [ - { - name: "Text", - icon: TextIcon, - command: (editor) => editor.chain().focus().clearNodes().run(), - // I feel like there has to be a more efficient way to do this – feel free to PR if you know how! - isActive: (editor) => - editor.isActive("paragraph") && !editor.isActive("bulletList") && !editor.isActive("orderedList"), - }, - { - name: "Heading 1", - icon: Heading1, - command: (editor) => editor.chain().focus().clearNodes().toggleHeading({ level: 1 }).run(), - isActive: (editor) => editor.isActive("heading", { level: 1 }), - }, - { - name: "Heading 2", - icon: Heading2, - command: (editor) => editor.chain().focus().clearNodes().toggleHeading({ level: 2 }).run(), - isActive: (editor) => editor.isActive("heading", { level: 2 }), - }, - { - name: "Heading 3", - icon: Heading3, - command: (editor) => editor.chain().focus().clearNodes().toggleHeading({ level: 3 }).run(), - isActive: (editor) => editor.isActive("heading", { level: 3 }), - }, - { - name: "To-do List", - icon: CheckSquare, - command: (editor) => editor.chain().focus().clearNodes().toggleTaskList().run(), - isActive: (editor) => editor.isActive("taskItem"), - }, - { - name: "Bullet List", - icon: ListOrdered, - command: (editor) => editor.chain().focus().clearNodes().toggleBulletList().run(), - isActive: (editor) => editor.isActive("bulletList"), - }, - { - name: "Numbered List", - icon: ListOrdered, - command: (editor) => editor.chain().focus().clearNodes().toggleOrderedList().run(), - isActive: (editor) => editor.isActive("orderedList"), - }, - { - name: "Quote", - icon: TextQuote, - command: (editor) => editor.chain().focus().clearNodes().toggleBlockquote().run(), - isActive: (editor) => editor.isActive("blockquote"), - }, - { - name: "Code", - icon: Code, - command: (editor) => editor.chain().focus().clearNodes().toggleCodeBlock().run(), - isActive: (editor) => editor.isActive("codeBlock"), - }, -]; -interface NodeSelectorProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const NodeSelector = ({ open, onOpenChange }: NodeSelectorProps) => { - const { editor } = useEditor(); - if (!editor) return null; - const activeItem = items.filter((item) => item.isActive(editor)).pop() ?? { - name: "Multiple", - }; - - return ( - - - - - - {items.map((item) => ( - { - item.command(editor); - onOpenChange(false); - }} - className="flex cursor-pointer items-center justify-between rounded-sm px-2 py-1 text-sm hover:bg-accent" - > -
-
- -
- {item.name} -
- {activeItem.name === item.name && } -
- ))} -
-
- ); -}; diff --git a/apps/web/app/(editor)/components/selectors/text-buttons.tsx b/apps/web/app/(editor)/components/selectors/text-buttons.tsx deleted file mode 100644 index f75d7b3c..00000000 --- a/apps/web/app/(editor)/components/selectors/text-buttons.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Button } from "@repo/ui/shadcn/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@repo/ui/shadcn/popover"; -import { BoldIcon, CodeIcon, ItalicIcon, StrikethroughIcon, UnderlineIcon } from "lucide-react"; -import { EditorBubbleItem, useEditor } from "novel"; -import type { SelectorItem } from "./node-selector"; -import { cn } from "@repo/ui/lib/utils"; - -export const TextButtons = () => { - const { editor } = useEditor(); - if (!editor) return null; - const items: SelectorItem[] = [ - { - name: "bold", - isActive: (editor) => editor.isActive("bold"), - command: (editor) => editor.chain().focus().toggleBold().run(), - icon: BoldIcon, - }, - { - name: "italic", - isActive: (editor) => editor.isActive("italic"), - command: (editor) => editor.chain().focus().toggleItalic().run(), - icon: ItalicIcon, - }, - { - name: "underline", - isActive: (editor) => editor.isActive("underline"), - command: (editor) => editor.chain().focus().toggleUnderline().run(), - icon: UnderlineIcon, - }, - { - name: "strike", - isActive: (editor) => editor.isActive("strike"), - command: (editor) => editor.chain().focus().toggleStrike().run(), - icon: StrikethroughIcon, - }, - { - name: "code", - isActive: (editor) => editor.isActive("code"), - command: (editor) => editor.chain().focus().toggleCode().run(), - icon: CodeIcon, - }, - ]; - return ( -
- {items.map((item) => ( - { - item.command(editor); - }} - > - - - ))} -
- ); -}; diff --git a/apps/web/app/(editor)/components/slash-command.tsx b/apps/web/app/(editor)/components/slash-command.tsx deleted file mode 100644 index 1bfb1690..00000000 --- a/apps/web/app/(editor)/components/slash-command.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { - CheckSquare, - Code, - Heading1, - Heading2, - Heading3, - ImageIcon, - List, - ListOrdered, - MessageSquarePlus, - Text, - TextQuote, - Youtube -} from "lucide-react"; -import { createSuggestionItems } from "novel/extensions"; -import { Command, renderItems } from "novel/extensions"; -import { uploadFn } from "./image-upload"; - -export const suggestionItems = createSuggestionItems([ - { - title: "Send Feedback", - description: "Let us know how we can improve.", - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).run(); - window.open("/feedback", "_blank"); - }, - }, - { - title: "Text", - description: "Just start typing with plain text.", - searchTerms: ["p", "paragraph"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run(); - }, - }, - { - title: "To-do List", - description: "Track tasks with a to-do list.", - searchTerms: ["todo", "task", "list", "check", "checkbox"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).toggleTaskList().run(); - }, - }, - { - title: "Heading 1", - description: "Big section heading.", - searchTerms: ["title", "big", "large"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run(); - }, - }, - { - title: "Heading 2", - description: "Medium section heading.", - searchTerms: ["subtitle", "medium"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run(); - }, - }, - { - title: "Heading 3", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run(); - }, - }, - { - title: "Bullet List", - description: "Create a simple bullet list.", - searchTerms: ["unordered", "point"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).toggleBulletList().run(); - }, - }, - { - title: "Numbered List", - description: "Create a list with numbering.", - searchTerms: ["ordered"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).toggleOrderedList().run(); - }, - }, - { - title: "Quote", - description: "Capture a quote.", - searchTerms: ["blockquote"], - icon: , - command: ({ editor, range }) => - editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run(), - }, - { - title: "Code", - description: "Capture a code snippet.", - searchTerms: ["codeblock"], - icon: , - command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), - }, - // { - // title: "Image", - // description: "Upload an image from your computer.", - // searchTerms: ["photo", "picture", "media"], - // icon: , - // command: ({ editor, range }) => { - // editor.chain().focus().deleteRange(range).run(); - // // upload image - // const input = document.createElement("input"); - // input.type = "file"; - // input.accept = "image/*"; - // input.onchange = async () => { - // if (input.files?.length) { - // const file = input.files[0]; - // const pos = editor.view.state.selection.from; - // uploadFn(file, editor.view, pos); - // } - // }; - // input.click(); - // }, - // }, - // { - // title: "Youtube", - // description: "Embed a Youtube video.", - // searchTerms: ["video", "youtube", "embed"], - // icon: , - // command: ({ editor, range }) => { - // const videoLink = prompt("Please enter Youtube Video Link"); - // //From https://regexr.com/3dj5t - // const ytregex = new RegExp( - // /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/, - // ); - - // if (ytregex.test(videoLink)) { - // editor - // .chain() - // .focus() - // .deleteRange(range) - // .setYoutubeVideo({ - // src: videoLink, - // }) - // .run(); - // } else { - // if (videoLink !== null) { - // alert("Please enter a correct Youtube Video Link"); - // } - // } - // }, - // }, -]); - -export const slashCommand = Command.configure({ - suggestion: { - items: () => suggestionItems, - render: renderItems, - }, -}); diff --git a/apps/web/app/(editor)/components/topbar.tsx b/apps/web/app/(editor)/components/topbar.tsx deleted file mode 100644 index 49b5179c..00000000 --- a/apps/web/app/(editor)/components/topbar.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client"; - -import { - AnimatePresence, - useMotionValueEvent, - useScroll, - motion, -} from "framer-motion"; -import React, { useState } from "react"; - -function Topbar({ - charsCount, - saveStatus, -}: { - charsCount: number | undefined; - saveStatus: string; -}) { - const [visible, setVisible] = useState(true); - - const { scrollYProgress } = useScroll(); - useMotionValueEvent(scrollYProgress, "change", (current) => { - if (typeof current === "number") { - let direction = current! - scrollYProgress.getPrevious()!; - - if (direction < 0 || direction === 1) { - setVisible(true); - } else { - setVisible(false); - } - } - }); - return ( -
- - -
-
- Untitled -
-
- {saveStatus} -
- {charsCount && ( -
- {`${charsCount} words`} -
- )} -
-
-
-
- ); -} - -export default Topbar; diff --git a/apps/web/app/(editor)/components/ui/asksvg.tsx b/apps/web/app/(editor)/components/ui/asksvg.tsx deleted file mode 100644 index aa38fe08..00000000 --- a/apps/web/app/(editor)/components/ui/asksvg.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' - -function Asksvg() { - return ( - - - - - ) -} - -export default Asksvg \ No newline at end of file diff --git a/apps/web/app/(editor)/components/ui/autocompletesvg.tsx b/apps/web/app/(editor)/components/ui/autocompletesvg.tsx deleted file mode 100644 index c433fcad..00000000 --- a/apps/web/app/(editor)/components/ui/autocompletesvg.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' - -function Autocompletesvg() { - return ( - - - - - ) -} - -export default Autocompletesvg \ No newline at end of file diff --git a/apps/web/app/(editor)/components/ui/crazy-spinner.tsx b/apps/web/app/(editor)/components/ui/crazy-spinner.tsx deleted file mode 100644 index 2e95deee..00000000 --- a/apps/web/app/(editor)/components/ui/crazy-spinner.tsx +++ /dev/null @@ -1,11 +0,0 @@ -const CrazySpinner = () => { - return ( -
-
-
-
-
- ); -}; - -export default CrazySpinner; diff --git a/apps/web/app/(editor)/components/ui/magic.tsx b/apps/web/app/(editor)/components/ui/magic.tsx deleted file mode 100644 index 04dce39e..00000000 --- a/apps/web/app/(editor)/components/ui/magic.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export default function Magic({ className }: { className: string }) { - return ( - - - - - ); -} diff --git a/apps/web/app/(editor)/components/ui/rewritesvg.tsx b/apps/web/app/(editor)/components/ui/rewritesvg.tsx deleted file mode 100644 index fad9eb90..00000000 --- a/apps/web/app/(editor)/components/ui/rewritesvg.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' - -function Rewritesvg() { - return ( - - - - ) -} - -export default Rewritesvg \ No newline at end of file diff --git a/apps/web/app/(editor)/components/ui/translatesvg.tsx b/apps/web/app/(editor)/components/ui/translatesvg.tsx deleted file mode 100644 index cde82da6..00000000 --- a/apps/web/app/(editor)/components/ui/translatesvg.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' - -function Translatesvg() { - return ( - - - - - ) -} - -export default Translatesvg \ No newline at end of file diff --git a/apps/web/app/(editor)/editor.tsx b/apps/web/app/(editor)/editor.tsx deleted file mode 100644 index f7f9a098..00000000 --- a/apps/web/app/(editor)/editor.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client"; -import { defaultEditorContent } from "./lib/content"; -import { EditorContent, EditorRoot, type JSONContent } from "novel"; -import { ImageResizer } from "novel/extensions"; -import { useEffect, useState } from "react"; -import { defaultExtensions } from "./components/extensions"; - -import { slashCommand } from "./components/slash-command"; -import { Updates } from "./lib/debouncedsave"; -import { editorProps } from "./lib/editorprops"; -import EditorCommands from "./components/editorcommands"; -import Aigenerate from "./components/aigenerate"; -import { useMotionValueEvent, useScroll } from "framer-motion"; -import Topbar from "./components/topbar"; - -const Editor = () => { - const [initialContent, setInitialContent] = useState( - null, - ); - const [saveStatus, setSaveStatus] = useState("Saved"); - const [charsCount, setCharsCount] = useState(); - const [visible, setVisible] = useState(true); - - useEffect(() => { - if (typeof window === "undefined") return; - const content = window.localStorage.getItem("novel-content"); - if (content) setInitialContent(JSON.parse(content)); - else setInitialContent(defaultEditorContent); - }, []); - - if (!initialContent) return <>Loading...; - - return ( -
- - - { - Updates({ editor, setCharsCount, setSaveStatus }); - }} - slotAfter={} - > - - - - -
- ); -}; - -export default Editor; diff --git a/apps/web/app/(editor)/editor/page.tsx b/apps/web/app/(editor)/editor/page.tsx deleted file mode 100644 index d0298065..00000000 --- a/apps/web/app/(editor)/editor/page.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import TailwindAdvancedEditor from "../editor"; -export default function Page() { - return ( -
- -
- ); -} diff --git a/apps/web/app/(editor)/layout.tsx b/apps/web/app/(editor)/layout.tsx deleted file mode 100644 index 1bf97715..00000000 --- a/apps/web/app/(editor)/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import "./styles/prosemirror.css"; -import "./styles/globals.css" -import type { ReactNode } from "react"; - - -export default function RootLayout({ children }: { children: ReactNode }) { - return ( -
- {children} -
- ); -} diff --git a/apps/web/app/(editor)/lib/content.ts b/apps/web/app/(editor)/lib/content.ts deleted file mode 100644 index 6464cfa1..00000000 --- a/apps/web/app/(editor)/lib/content.ts +++ /dev/null @@ -1,231 +0,0 @@ -export const defaultEditorContent = { - type: "doc", - content: [ - { - type: "heading", - attrs: { level: 2 }, - content: [{ type: "text", text: "Introducing Novel" }], - }, - { - type: "paragraph", - content: [ - { - type: "text", - marks: [ - { - type: "link", - attrs: { - href: "https://github.com/steven-tey/novel", - target: "_blank", - }, - }, - ], - text: "Novel", - }, - { - type: "text", - text: " is a Notion-style WYSIWYG editor with AI-powered autocompletion. Built with ", - }, - { - type: "text", - marks: [ - { - type: "link", - attrs: { - href: "https://tiptap.dev/", - target: "_blank", - }, - }, - ], - text: "Tiptap", - }, - { type: "text", text: " + " }, - { - type: "text", - marks: [ - { - type: "link", - attrs: { - href: "https://sdk.vercel.ai/docs", - target: "_blank", - }, - }, - ], - text: "Vercel AI SDK", - }, - { type: "text", text: "." }, - ], - }, - { - type: "heading", - attrs: { level: 3 }, - content: [{ type: "text", text: "Installation" }], - }, - { - type: "codeBlock", - attrs: { language: null }, - content: [{ type: "text", text: "npm i novel" }], - }, - { - type: "heading", - attrs: { level: 3 }, - content: [{ type: "text", text: "Usage" }], - }, - { - type: "codeBlock", - attrs: { language: null }, - content: [ - { - type: "text", - text: 'import { Editor } from "novel";\n\nexport default function App() {\n return (\n \n )\n}', - }, - ], - }, - { - type: "heading", - attrs: { level: 3 }, - content: [{ type: "text", text: "Features" }], - }, - { - type: "orderedList", - attrs: { tight: true, start: 1 }, - content: [ - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Slash menu & bubble menu" }], - }, - ], - }, - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [ - { type: "text", text: "AI autocomplete (type " }, - { type: "text", marks: [{ type: "code" }], text: "++" }, - { - type: "text", - text: " to activate, or select from slash menu)", - }, - ], - }, - ], - }, - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [ - { - type: "text", - text: "Image uploads (drag & drop / copy & paste, or select from slash menu) ", - }, - ], - }, - ], - }, - ], - }, - { - type: "image", - attrs: { - src: "https://public.blob.vercel-storage.com/pJrjXbdONOnAeZAZ/banner-2wQk82qTwyVgvlhTW21GIkWgqPGD2C.png", - alt: "banner.png", - title: "banner.png", - width: null, - height: null, - }, - }, - { type: "horizontalRule" }, - { - type: "heading", - attrs: { level: 3 }, - content: [{ type: "text", text: "Learn more" }], - }, - { - type: "taskList", - content: [ - { - type: "taskItem", - attrs: { checked: false }, - content: [ - { - type: "paragraph", - content: [ - { type: "text", text: "Star us on " }, - { - type: "text", - marks: [ - { - type: "link", - attrs: { - href: "https://github.com/steven-tey/novel", - target: "_blank", - }, - }, - ], - text: "GitHub", - }, - ], - }, - ], - }, - { - type: "taskItem", - attrs: { checked: false }, - content: [ - { - type: "paragraph", - content: [ - { type: "text", text: "Install the " }, - { - type: "text", - marks: [ - { - type: "link", - attrs: { - href: "https://www.npmjs.com/package/novel", - target: "_blank", - }, - }, - ], - text: "NPM package", - }, - ], - }, - ], - }, - { - type: "taskItem", - attrs: { checked: false }, - content: [ - { - type: "paragraph", - content: [ - { - type: "text", - marks: [ - { - type: "link", - attrs: { - href: "https://vercel.com/templates/next.js/novel", - target: "_blank", - }, - }, - ], - text: "Deploy your own", - }, - { type: "text", text: " to Vercel" }, - ], - }, - ], - }, - ], - }, - ], -}; diff --git a/apps/web/app/(editor)/lib/debouncedsave.ts b/apps/web/app/(editor)/lib/debouncedsave.ts deleted file mode 100644 index 6490c6c4..00000000 --- a/apps/web/app/(editor)/lib/debouncedsave.ts +++ /dev/null @@ -1,20 +0,0 @@ -import hljs from 'highlight.js' -import { debounce } from 'tldraw'; -import { useDebouncedCallback } from "use-debounce"; - -export const Updates = debounce(({editor, setCharsCount, setSaveStatus})=> { - const json = editor.getJSON(); - setCharsCount(editor.storage.characterCount.words()); - window.localStorage.setItem("html-content", highlightCodeblocks(editor.getHTML())); - window.localStorage.setItem("novel-content", JSON.stringify(json)); - window.localStorage.setItem("markdown", editor.storage.markdown.getMarkdown()); - setSaveStatus("Saved"); -}, 500) - -export const highlightCodeblocks = (content: string) => { - const doc = new DOMParser().parseFromString(content, 'text/html'); - doc.querySelectorAll('pre code').forEach((el) => { - hljs.highlightElement(el); - }); - return new XMLSerializer().serializeToString(doc); -}; \ No newline at end of file diff --git a/apps/web/app/(editor)/lib/editorprops.ts b/apps/web/app/(editor)/lib/editorprops.ts deleted file mode 100644 index 00d89264..00000000 --- a/apps/web/app/(editor)/lib/editorprops.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { handleCommandNavigation } from "novel/extensions"; -import { handleImageDrop, handleImagePaste } from "novel/plugins"; -import { uploadFn } from "../components/image-upload"; -import { EditorView } from "prosemirror-view"; - -export const editorProps = { - handleDOMEvents: { - keydown: (_view: EditorView, event: KeyboardEvent) => handleCommandNavigation(event), - }, - handlePaste: (view: EditorView, event: ClipboardEvent) => handleImagePaste(view, event, uploadFn), - handleDrop: (view: EditorView, event: DragEvent, slice, moved:boolean) => handleImageDrop(view, event, moved, uploadFn), - attributes: { - class: - "prose prose-lg dark:prose-invert prose-headings:font-title font-default focus:outline-none max-w-full", - }, -} \ No newline at end of file diff --git a/apps/web/app/(editor)/lib/use-local-storage.ts b/apps/web/app/(editor)/lib/use-local-storage.ts deleted file mode 100644 index 5f2ebeb9..00000000 --- a/apps/web/app/(editor)/lib/use-local-storage.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useState } from "react"; - -const useLocalStorage = ( - key: string, - initialValue: T, - // eslint-disable-next-line no-unused-vars -): [T, (value: T) => void] => { - const [storedValue, setStoredValue] = useState(initialValue); - - useEffect(() => { - // Retrieve from localStorage - const item = window.localStorage.getItem(key); - if (item) { - setStoredValue(JSON.parse(item)); - } - }, [key]); - - const setValue = (value: T) => { - // Save state - setStoredValue(value); - // Save to localStorage - window.localStorage.setItem(key, JSON.stringify(value)); - }; - return [storedValue, setValue]; -}; - -export default useLocalStorage; diff --git a/apps/web/app/(editor)/styles/globals.css b/apps/web/app/(editor)/styles/globals.css deleted file mode 100644 index 336e2dae..00000000 --- a/apps/web/app/(editor)/styles/globals.css +++ /dev/null @@ -1,168 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; - - --radius: 0.5rem; - - --novel-highlight-default: #ffffff; - --novel-highlight-purple: #f6f3f8; - --novel-highlight-red: #fdebeb; - --novel-highlight-yellow: #fbf4a2; - --novel-highlight-blue: #c1ecf9; - --novel-highlight-green: #acf79f; - --novel-highlight-orange: #faebdd; - --novel-highlight-pink: #faf1f5; - --novel-highlight-gray: #f1f1ef; - } - - .dark { - --background: #171B1F; - --foreground: 210 40% 98%; - - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; - - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - - --novel-highlight-default: #000000; - --novel-highlight-purple: #3f2c4b; - --novel-highlight-red: #5c1a1a; - --novel-highlight-yellow: #5c4b1a; - --novel-highlight-blue: #1a3d5c; - --novel-highlight-green: #1a5c20; - --novel-highlight-orange: #5c3a1a; - --novel-highlight-pink: #5c1a3a; - --novel-highlight-gray: #3a3a3a; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} - - -pre { - background: #0d0d0d; - border-radius: 0.5rem; - color: #fff; - font-family: "JetBrainsMono", monospace; - padding: 0.75rem 1rem; - - code { - background: none; - color: inherit; - font-size: 0.8rem; - padding: 0; - } - - .hljs-comment, - .hljs-quote { - color: #616161; - } - - .hljs-variable, - .hljs-template-variable, - .hljs-attribute, - .hljs-tag, - .hljs-name, - .hljs-regexp, - .hljs-link, - .hljs-name, - .hljs-selector-id, - .hljs-selector-class { - color: #f98181; - } - - .hljs-number, - .hljs-meta, - .hljs-built_in, - .hljs-builtin-name, - .hljs-literal, - .hljs-type, - .hljs-params { - color: #fbbc88; - } - - .hljs-string, - .hljs-symbol, - .hljs-bullet { - color: #b9f18d; - } - - .hljs-title, - .hljs-section { - color: #faf594; - } - - .hljs-keyword, - .hljs-selector-tag { - color: #70cff8; - } - - .hljs-emphasis { - font-style: italic; - } - - .hljs-strong { - font-weight: 700; - } -} -::-webkit-scrollbar{ - width: 0; -} - diff --git a/apps/web/app/(editor)/styles/prosemirror.css b/apps/web/app/(editor)/styles/prosemirror.css deleted file mode 100644 index 7298c98b..00000000 --- a/apps/web/app/(editor)/styles/prosemirror.css +++ /dev/null @@ -1,203 +0,0 @@ -.ProseMirror { - @apply p-12 px-8 sm:px-12; -} - -.ProseMirror .is-editor-empty:first-child::before { - content: attr(data-placeholder); - float: left; - color: hsl(var(--muted-foreground)); - pointer-events: none; - height: 0; -} -.ProseMirror .is-empty::before { - content: attr(data-placeholder); - float: left; - color: hsl(var(--muted-foreground)); - pointer-events: none; - height: 0; -} - -/* Custom image styles */ - -.ProseMirror img { - transition: filter 0.1s ease-in-out; - - &:hover { - cursor: pointer; - filter: brightness(90%); - } - - &.ProseMirror-selectednode { - outline: 3px solid #5abbf7; - filter: brightness(90%); - } -} - -.img-placeholder { - position: relative; - - &:before { - content: ""; - box-sizing: border-box; - position: absolute; - top: 50%; - left: 50%; - width: 36px; - height: 36px; - border-radius: 50%; - border: 3px solid var(--novel-stone-200); - border-top-color: var(--novel-stone-800); - animation: spinning 0.6s linear infinite; - } -} - -@keyframes spinning { - to { - transform: rotate(360deg); - } -} - -/* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */ - -ul[data-type="taskList"] li > label { - margin-right: 0.2rem; - user-select: none; -} - -@media screen and (max-width: 768px) { - ul[data-type="taskList"] li > label { - margin-right: 0.5rem; - } -} - -ul[data-type="taskList"] li > label input[type="checkbox"] { - -webkit-appearance: none; - appearance: none; - background-color: hsl(var(--background)); - margin: 0; - cursor: pointer; - width: 1.2em; - height: 1.2em; - position: relative; - top: 5px; - border: 2px solid hsl(var(--border)); - margin-right: 0.3rem; - display: grid; - place-content: center; - - &:hover { - background-color: hsl(var(--accent)); - } - - &:active { - background-color: hsl(var(--accent)); - } - - &::before { - content: ""; - width: 0.65em; - height: 0.65em; - transform: scale(0); - transition: 120ms transform ease-in-out; - box-shadow: inset 1em 1em; - transform-origin: center; - clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); - } - - &:checked::before { - transform: scale(1); - } -} - -ul[data-type="taskList"] li[data-checked="true"] > div > p { - color: var(--muted-foreground); - text-decoration: line-through; - text-decoration-thickness: 2px; -} - -/* Overwrite tippy-box original max-width */ - -.tippy-box { - max-width: 400px !important; -} - -.ProseMirror:not(.dragging) .ProseMirror-selectednode { - outline: none !important; - background-color: var(--novel-highlight-blue); - transition: background-color 0.2s; - box-shadow: none; -} - -.drag-handle { - position: fixed; - opacity: 1; - transition: opacity ease-in 0.2s; - border-radius: 0.25rem; - - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(0, 0, 0, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E"); - background-size: calc(0.5em + 0.375rem) calc(0.5em + 0.375rem); - background-repeat: no-repeat; - background-position: center; - width: 1.2rem; - height: 1.5rem; - z-index: 50; - cursor: grab; - - &:hover { - background-color: var(--novel-stone-100); - transition: background-color 0.2s; - } - - &:active { - background-color: var(--novel-stone-200); - transition: background-color 0.2s; - cursor: grabbing; - } - - &.hide { - opacity: 0; - pointer-events: none; - } - - @media screen and (max-width: 600px) { - display: none; - pointer-events: none; - } -} - -.dark .drag-handle { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(255, 255, 255, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E"); -} - -/* Custom Youtube Video CSS */ -iframe { - border: 8px solid #ffd00027; - border-radius: 4px; - min-width: 200px; - min-height: 200px; - display: block; - outline: 0px solid transparent; -} - -div[data-youtube-video] > iframe { - cursor: move; - aspect-ratio: 16 / 9; - width: 100%; -} - -.ProseMirror-selectednode iframe { - transition: outline 0.15s; - outline: 6px solid #fbbf24; -} - -@media only screen and (max-width: 480px) { - div[data-youtube-video] > iframe { - max-height: 50px; - } -} - -@media only screen and (max-width: 720px) { - div[data-youtube-video] > iframe { - max-height: 100px; - } -} \ No newline at end of file diff --git a/apps/web/public/logo.svg b/apps/web/public/logo.svg index 6081634d..add59158 100644 --- a/apps/web/public/logo.svg +++ b/apps/web/public/logo.svg @@ -1,4 +1,4 @@ + fill="#545B62"/> \ No newline at end of file diff --git a/package.json b/package.json index 7ceb7192..35b90102 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@aws-sdk/s3-request-presigner": "^3.577.0", "@cloudflare/puppeteer": "^0.0.11", "@headlessui/react": "^2.0.4", + "@heroicons/react": "^2.1.4", "@hono/swagger-ui": "^0.2.2", "@hookform/resolvers": "^3.4.2", "@iarna/toml": "^2.2.5", @@ -55,11 +56,13 @@ "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@tldraw/assets": "^2.2.0", diff --git a/packages/tailwind-config/globals.css b/packages/tailwind-config/globals.css index a77eacbb..d09b120b 100644 --- a/packages/tailwind-config/globals.css +++ b/packages/tailwind-config/globals.css @@ -205,3 +205,19 @@ body { .hljs-title { color: #f22c3d; } + +/* no scrollbar visibility */ +.no-scrollbar::-webkit-scrollbar { + width: 0px; +} + +::-moz-selection { + /* Code for Firefox */ + color: #369dfd; + background: #21303d; +} + +::selection { + color: #369dfd; + background: #21303d; +} diff --git a/packages/ui/components/QueryInput.tsx b/packages/ui/components/QueryInput.tsx new file mode 100644 index 00000000..ba476dda --- /dev/null +++ b/packages/ui/components/QueryInput.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import Divider from "../shadcn/divider"; +import { ArrowRightIcon } from "../icons"; +import Image from "next/image"; + +function QueryInput() { + return ( +
+
+ {/* input and action button */} +
+