From a73e4cae0effa97b5a01828c033e64815be5c96d Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Fri, 27 Dec 2024 04:11:07 +0100 Subject: [PATCH] hide chat window with expand/collapse button and key bindings --- README.md | 4 +- components.json | 21 ++ src/app/globals.css | 11 + src/app/page.tsx | 64 +++++- src/components/ChatBox.tsx | 88 +++++--- src/components/KeyboardShortcutHint.tsx | 9 + src/components/Spreadsheet.tsx | 13 +- src/components/ui/button.tsx | 57 +++++ src/lib/spreadsheet/config.ts | 4 +- src/lib/spreadsheet/themes.ts | 16 ++ src/lib/utils.ts | 6 + src/styles/spreadsheet-themes.css | 279 ++++++++++++++++++++++++ 12 files changed, 523 insertions(+), 49 deletions(-) create mode 100644 components.json create mode 100644 src/components/KeyboardShortcutHint.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/lib/spreadsheet/themes.ts create mode 100644 src/lib/utils.ts create mode 100644 src/styles/spreadsheet-themes.css diff --git a/README.md b/README.md index 603c6f8..c2ba499 100644 --- a/README.md +++ b/README.md @@ -57,5 +57,5 @@ npm run electron-start - [ ] Other kinds of operations (hide rows where, hide columns where, sort table by attribute, etc.) - [ ] Support for multiple sheets (or select sheet to use on import) - [ ] Make more of the top toolbar buttons work (first check out of the box hands on table functionality) -- [ ] Keybindings / hotkeys -- [ ] Hide chat window with expand / collapse button +- [] Keybindings / hotkeys +- [x] Hide chat window with expand / collapse button diff --git a/components.json b/components.json new file mode 100644 index 0000000..23c7e8a --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css index d82910c..1b811b8 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -36,3 +36,14 @@ body { .chat-message-container::-webkit-scrollbar-thumb:hover { background: #555; } +.handsontable { + z-index: 1 !important; +} + +.handsontable .wtHolder { + z-index: 1 !important; +} + +.handsontable .htCore { + z-index: 1 !important; +} diff --git a/src/app/page.tsx b/src/app/page.tsx index c83ea99..cde6be1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,11 +7,12 @@ import { SpreadsheetProvider, useSpreadsheet, } from "@/context/SpreadsheetContext"; +import { MessageCircle } from "lucide-react"; const Spreadsheet = dynamic(() => import("@/components/Spreadsheet"), { ssr: false, loading: () => ( -
+
Loading spreadsheet...
), @@ -30,8 +31,33 @@ const SpreadsheetApp = () => { const [isElectron, setIsElectron] = useState(false); const [electronAPIAvailable, setElectronAPIAvailable] = useState(false); const [chatHistory, setChatHistory] = useState([]); + const [isChatOpen, setIsChatOpen] = useState(false); - // Load chat history from localStorage on mount + // Keyboard shortcut handler + useEffect(() => { + const handleKeyPress = (e: KeyboardEvent) => { + if (e.ctrlKey && e.shiftKey && e.key === "?") { + setIsChatOpen((prev) => !prev); + } + }; + window.addEventListener("keydown", handleKeyPress); + return () => window.removeEventListener("keydown", handleKeyPress); + }, []); + + // Load chat state + useEffect(() => { + const savedState = localStorage.getItem("chatOpen"); + if (savedState) { + setIsChatOpen(JSON.parse(savedState)); + } + }, []); + + // Save chat state + useEffect(() => { + localStorage.setItem("chatOpen", JSON.stringify(isChatOpen)); + }, [isChatOpen]); + + // Load chat history useEffect(() => { const savedHistory = localStorage.getItem("chatHistory"); if (savedHistory) { @@ -50,13 +76,13 @@ const SpreadsheetApp = () => { } }, []); - // Save chat history to localStorage when it changes + // Save chat history useEffect(() => { localStorage.setItem("chatHistory", JSON.stringify(chatHistory)); }, [chatHistory]); + // Check Electron availability useEffect(() => { - // Check for Electron API availability after component mounts const checkElectronAvailability = () => { const hasElectronAPI = typeof window !== "undefined" && !!window.electron; setElectronAPIAvailable(hasElectronAPI); @@ -65,7 +91,7 @@ const SpreadsheetApp = () => { if (hasElectronAPI) { console.log("Electron environment detected!"); window.electron - .invoke("test-connection", { test: true }) + ?.invoke("test-connection", { test: true }) .then((result) => console.log("Electron test successful:", result)) .catch((err) => console.error("Electron test failed:", err)); } else { @@ -81,7 +107,6 @@ const SpreadsheetApp = () => { isElectron, hasElectronAPI: electronAPIAvailable, }); - try { let updates; @@ -153,16 +178,26 @@ const SpreadsheetApp = () => { }; return ( -
-
+
+

Magic Spreadsheet

-
-
+
+
-
+ {/* Updated ChatBox container */} +
{
+
); }; diff --git a/src/components/ChatBox.tsx b/src/components/ChatBox.tsx index ace6129..2741e73 100644 --- a/src/components/ChatBox.tsx +++ b/src/components/ChatBox.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { CellUpdate } from "@/types/api"; +import { Send, Trash2, Loader2 } from "lucide-react"; interface ChatMessage { id: string; @@ -21,7 +22,6 @@ const ChatBox = ({ onSend, chatHistory, clearHistory }: ChatBoxProps) => { const handleSend = async () => { if (message.trim()) { setIsLoading(true); - try { await onSend(message); } catch (error) { @@ -40,59 +40,81 @@ const ChatBox = ({ onSend, chatHistory, clearHistory }: ChatBoxProps) => { }; return ( -
- {/* Chat Header */} -
-

Chat History

+
+ {/* Header */} +
+
+

AI Assistant

+

+ Ask me about spreadsheet formulas +

+
{/* Chat History */} -
- {chatHistory.map((chat) => ( -
-
-
-

{chat.text}

- - {new Date(chat.timestamp).toLocaleTimeString()} - -
-
-
-
-
-                  {chat.response}
-                
-
-
+
+ {chatHistory.length === 0 ? ( +
+

No messages yet

+

+ Try asking me to create formulas or analyze your data +

- ))} + ) : ( + chatHistory.map((chat) => ( +
+ {/* User Message */} +
+
+

{chat.text}

+ + {new Date(chat.timestamp).toLocaleTimeString()} + +
+
+ {/* AI Response */} +
+
+
+                    {chat.response}
+                  
+
+
+
+ )) + )}
{/* Input Area */} -
-
+
+
setMessage(e.target.value)} onKeyDown={handleKeyDown} - className="flex-1 p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Ask the LLM to create a formula..." + className="flex-1 px-4 py-2 border border-gray-200 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm" + placeholder="Type your message..." disabled={isLoading} />
diff --git a/src/components/KeyboardShortcutHint.tsx b/src/components/KeyboardShortcutHint.tsx new file mode 100644 index 0000000..6246419 --- /dev/null +++ b/src/components/KeyboardShortcutHint.tsx @@ -0,0 +1,9 @@ +const KeyboardShortcutHint = () => { + return ( +
+ Press Ctrl+Shift+/ to toggle chat +
+ ); +}; + +export default KeyboardShortcutHint; diff --git a/src/components/Spreadsheet.tsx b/src/components/Spreadsheet.tsx index e6f1086..7577b6d 100644 --- a/src/components/Spreadsheet.tsx +++ b/src/components/Spreadsheet.tsx @@ -128,6 +128,15 @@ const Spreadsheet = ({ onDataChange, initialData }: SpreadsheetProps) => { useEffect(() => { if (spreadsheetRef.current && !hotInstanceRef.current) { try { + const config = getInitialConfig(currentData); + config.afterChange = (changes: any) => { + if (changes) { + const currentData = hotInstanceRef.current?.getData(); + if (currentData && onDataChange) { + onDataChange(currentData); + } + } + }; hotInstanceRef.current = new Handsontable( spreadsheetRef.current, getInitialConfig(currentData), @@ -152,7 +161,7 @@ const Spreadsheet = ({ onDataChange, initialData }: SpreadsheetProps) => { }, [currentData]); return ( -
+
{ onSearch={() => setShowSearch(true)} onSort={() => dataHandlers.handleSort(hotInstanceRef.current)} /> -
+
{showSearch && ( , + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/lib/spreadsheet/config.ts b/src/lib/spreadsheet/config.ts index c6ee820..452a396 100644 --- a/src/lib/spreadsheet/config.ts +++ b/src/lib/spreadsheet/config.ts @@ -11,7 +11,7 @@ export const getInitialConfig = (data: any[][]): GridSettings => { rowHeaders: true, colHeaders: true, width: "100%", - height: "70vh", + height: "100%", licenseKey: "non-commercial-and-evaluation", formulas: { engine: hyperformulaInstance, @@ -36,5 +36,7 @@ export const getInitialConfig = (data: any[][]): GridSettings => { const cellProperties: any = {}; return cellProperties; }, + observeDomVisibility: true, + observeChanges: true, }; }; diff --git a/src/lib/spreadsheet/themes.ts b/src/lib/spreadsheet/themes.ts new file mode 100644 index 0000000..c1186df --- /dev/null +++ b/src/lib/spreadsheet/themes.ts @@ -0,0 +1,16 @@ +export const themes: any = { + default: { + name: "default", + className: "default-theme", + }, + dark: { + name: "dark", + className: "dark-theme", + }, + custom: { + name: "custom", + className: "custom-theme", + }, +} as const; + +export type ThemeType = keyof typeof themes; diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/styles/spreadsheet-themes.css b/src/styles/spreadsheet-themes.css new file mode 100644 index 0000000..eefd2f9 --- /dev/null +++ b/src/styles/spreadsheet-themes.css @@ -0,0 +1,279 @@ +.handsontable.default-theme { + background: #ffffff; +} + +.handsontable.default-theme th { + background-color: #f3f4f6; + color: #374151; + border-color: #e5e7eb; +} + +.handsontable.default-theme td { + background-color: #ffffff; + color: #1f2937; + border-color: #e5e7eb; +} + +.handsontable.default-theme .currentRow { + background-color: #f9fafb; +} + +.handsontable.default-theme .currentCol { + background-color: #f9fafb; +} + +/* Dark Theme */ +.handsontable.dark-theme { + background: #1f2937; +} + +.handsontable.dark-theme th { + background-color: #374151; + color: #e5e7eb; + border-color: #4b5563; +} + +.handsontable.dark-theme td { + background-color: #1f2937; + color: #f9fafb; + border-color: #4b5563; +} + +.handsontable.dark-theme .currentRow { + background-color: #374151; +} + +.handsontable.dark-theme .currentCol { + background-color: #374151; +} + +.handsontable.dark-theme .handsontable-row-header, +.handsontable.dark-theme .handsontable-col-header { + background-color: #374151; + color: #e5e7eb; +} + +/* Custom Theme (Blue) */ +.handsontable.custom-theme { + background: #f0f9ff; +} + +.handsontable.custom-theme th { + background-color: #0ea5e9; + color: white; + border-color: #7dd3fc; +} + +.handsontable.custom-theme td { + background-color: #ffffff; + color: #0c4a6e; + border-color: #7dd3fc; +} + +.handsontable.custom-theme .currentRow { + background-color: #f0f9ff; +} + +.handsontable.custom-theme .currentCol { + background-color: #f0f9ff; +} + +/* Common Styles */ +.handsontable { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; + font-size: 14px; +} + +/* Selection Styles */ +.handsontable .area { + background-color: #0ea5e9 !important; + opacity: 0.1; +} + +.handsontable .area.corner { + background-color: #0ea5e9 !important; + opacity: 0.2; +} + +/* Text Formatting */ +.handsontable td strong, +.handsontable td .bold { + font-weight: bold !important; +} + +.handsontable td em, +.handsontable td .italic { + font-style: italic !important; +} + +.handsontable td u, +.handsontable td .underline { + text-decoration: underline !important; +} + +/* Cell Alignment */ +.handsontable td.htLeft { + text-align: left !important; +} + +.handsontable td.htCenter { + text-align: center !important; +} + +.handsontable td.htRight { + text-align: right !important; +} + +/* Search Highlighting */ +.handsontable td.search-highlight { + background-color: rgba(255, 255, 0, 0.5) !important; +} + +/* Header Highlighting */ +.handsontable th.ht__highlight { + background-color: #0ea5e9 !important; + color: white !important; +} + +/* Active Cell */ +.handsontable td.current { + border: 2px solid #0ea5e9 !important; +} + +/* Invalid Cell */ +.handsontable td.htInvalid { + background-color: #fee2e2 !important; +} + +/* Read-only Cell */ +.handsontable td.htReadOnly { + background-color: #f3f4f6 !important; +} + +/* Merged Cells */ +.handsontable td.htMergedCell { + background-color: #f9fafb !important; +} + +/* Context Menu */ +.handsontable .htContextMenu { + background: #ffffff; + border: 1px solid #e5e7eb; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.handsontable.dark-theme .htContextMenu { + background: #374151; + border: 1px solid #4b5563; + color: #f9fafb; +} + +.handsontable .htContextMenu .ht_master .htCore td { + padding: 8px 12px; + border: none; +} + +.handsontable .htContextMenu .ht_master .htCore td:hover { + background: #f3f4f6; +} + +.handsontable.dark-theme .htContextMenu .ht_master .htCore td:hover { + background: #4b5563; +} + +/* Scrollbars */ +.handsontable ::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.handsontable ::-webkit-scrollbar-track { + background: #f3f4f6; +} + +.handsontable.dark-theme ::-webkit-scrollbar-track { + background: #374151; +} + +.handsontable ::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; +} + +.handsontable.dark-theme ::-webkit-scrollbar-thumb { + background: #4b5563; +} + +.handsontable ::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +.handsontable.dark-theme ::-webkit-scrollbar-thumb:hover { + background: #6b7280; +} + +/* Comments */ +.handsontable .htCommentCell:after { + border-color: #ef4444 transparent transparent #ef4444 !important; +} + +/* Column Resize Handle */ +.handsontable .manualColumnResizer:hover, +.handsontable .manualColumnResizer.active { + background-color: #0ea5e9 !important; +} + +/* Row Resize Handle */ +.handsontable .manualRowResizer:hover, +.handsontable .manualRowResizer.active { + background-color: #0ea5e9 !important; +} + +/* Focus Border */ +.handsontable .wtBorder.current { + background-color: #0ea5e9 !important; +} + +.handsontable .wtBorder.area { + background-color: #0ea5e9 !important; + opacity: 0.2; +} + +/* Selection Border */ +.handsontable .wtBorder.selection { + background-color: #0ea5e9 !important; +} + +/* Fill Handle */ +.handsontable .wtBorder.corner { + background-color: #0ea5e9 !important; +} + +/* Autocomplete */ +.handsontableEditor.autocompleteEditor { + background: #ffffff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.handsontableEditor.autocompleteEditor .ht_master .htCore td { + padding: 8px 12px; + border: none; +} + +.handsontableEditor.autocompleteEditor .ht_master .htCore td:hover { + background: #f3f4f6; +} + +.handsontable.dark-theme .handsontableEditor.autocompleteEditor { + background: #374151; + color: #f9fafb; +} + +.handsontable.dark-theme + .handsontableEditor.autocompleteEditor + .ht_master + .htCore + td:hover { + background: #4b5563; +}