Skip to content

Commit

Permalink
feat: add keyboard events and tooltip (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
RainyNight9 authored Dec 20, 2024
1 parent 4b6965e commit dd2260a
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 51 deletions.
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
"maximizable",
"Minimizable",
"nord",
"nowrap",
"nspanel",
"nsstring",
"partialize",
"Raycast",
"rehype",
"rgba",
"serde",
"tailwindcss",
"tauri",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ fn change_shortcut<R: Runtime>(
.on_shortcut(shortcut, move |_app, scut, event| {
if scut == &shortcut {
if let ShortcutState::Pressed = event.state() {
if main_window.is_focused().unwrap() {
if main_window.is_visible().unwrap() {
main_window.hide().unwrap();
} else {
main_window.show().unwrap();
Expand Down
26 changes: 17 additions & 9 deletions src/components/SearchChat/AutoResizeTextarea.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import React, { useEffect, useRef } from "react";
import { useEffect, useRef, useImperativeHandle, forwardRef } from "react";

interface AutoResizeTextareaProps {
input: string;
setInput: (value: string) => void;
handleKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
}

const AutoResizeTextarea: React.FC<AutoResizeTextareaProps> = ({
input,
setInput,
handleKeyDown,
}) => {
// Forward ref to allow parent to interact with this component
const AutoResizeTextarea = forwardRef<
{ reset: () => void; focus: () => void },
AutoResizeTextareaProps
>(({ input, setInput }, ref) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);

useEffect(() => {
Expand All @@ -21,6 +20,16 @@ const AutoResizeTextarea: React.FC<AutoResizeTextareaProps> = ({
}
}, [input]);

// Expose methods to the parent via ref
useImperativeHandle(ref, () => ({
reset: () => {
setInput("");
},
focus: () => {
textareaRef.current?.focus();
},
}));

return (
<textarea
ref={textareaRef}
Expand All @@ -32,7 +41,6 @@ const AutoResizeTextarea: React.FC<AutoResizeTextareaProps> = ({
placeholder="Ask whatever you want ..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
rows={1}
style={{
resize: "none", // Prevent manual resize
Expand All @@ -42,6 +50,6 @@ const AutoResizeTextarea: React.FC<AutoResizeTextareaProps> = ({
}}
/>
);
};
});

export default AutoResizeTextarea;
5 changes: 2 additions & 3 deletions src/components/SearchChat/ChatSwitch.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useEffect} from "react";
import React, { useEffect } from "react";
import { Bot, Search } from "lucide-react";

interface ChatSwitchProps {
Expand All @@ -7,13 +7,12 @@ interface ChatSwitchProps {
}

const ChatSwitch: React.FC<ChatSwitchProps> = ({ isChatMode, onChange }) => {

useEffect(() => {
const handleKeydown = (event: KeyboardEvent) => {
if (event.metaKey && event.key === "t") {
event.preventDefault();
console.log("Switch mode triggered");
handleToggle()
handleToggle();
}
};

Expand Down
154 changes: 129 additions & 25 deletions src/components/SearchChat/InputBox.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Library, Mic, Send, Plus, AudioLines, Image } from "lucide-react";
import { useRef, type KeyboardEvent } from "react";
import { useRef, useState, useEffect } from "react";

import ChatSwitch from "../SearchChat/ChatSwitch";
import AutoResizeTextarea from "./AutoResizeTextarea";
Expand All @@ -26,36 +26,86 @@ export default function ChatInput({
disabledChange,
}: ChatInputProps) {
const inputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<{ reset: () => void; focus: () => void }>(null);

const { curChatEnd } = useChatStore();

const [isCommandPressed, setIsCommandPressed] = useState(false);

useEffect(() => {
const handleKeyDown = (e: any) => {
if (e.code === "MetaLeft" || e.code === "MetaRight") {
setIsCommandPressed(true);
}

if (e.metaKey) {
switch (e.code) {
case "KeyI":
if (isChatMode) {
textareaRef.current?.focus();
} else {
inputRef.current?.focus();
}
break;
case "KeyM":
console.log("KeyM");
break;
case "Enter":
isChatMode && handleSubmit();
break;
case "KeyO":
console.log("KeyO");
break;
case "KeyU":
console.log("KeyU");
break;
case "KeyN":
console.log("KeyN");
break;
case "KeyG":
console.log("KeyG");
break;
default:
break;
}
}
};

const handleKeyUp = (e: any) => {
if (e.code === "MetaLeft" || e.code === "MetaRight") {
setIsCommandPressed(false);
}
};

window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);

return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
}, [isChatMode]);

const handleSubmit = () => {
if (inputValue.trim() && !disabled) {
onSend(inputValue.trim());
}
};

const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
};

const openChatAI = async () => {
console.log("Chat AI opened.");
};

return (
<div className="w-full rounded-xl overflow-hidden">
<div className="w-full rounded-xl overflow-hidden relative">
<div className="rounded-xl">
<div className="p-[13px] flex items-center dark:text-[#D8D8D8] bg-white dark:bg-[#202126] rounded-xl transition-all">
<div className="flex flex-wrap gap-2 flex-1 items-center">
<div className="p-[13px] flex items-center dark:text-[#D8D8D8] bg-white dark:bg-[#202126] rounded-xl transition-all relative">
<div className="flex flex-wrap gap-2 flex-1 items-center relative">
{isChatMode ? (
<AutoResizeTextarea
ref={textareaRef}
input={inputValue}
setInput={changeInput}
handleKeyDown={handleKeyDown}
/>
) : (
<input
Expand All @@ -73,6 +123,20 @@ export default function ChatInput({
}}
/>
)}
{isCommandPressed ? (
<div
className={`absolute bg-black bg-opacity-70 text-white font-bold px-2.5 py-1 rounded-md text-xs transition-opacity duration-200`}
>
⌘ + i
</div>
) : null}
{isChatMode && isCommandPressed ? (
<div
className={`absolute right-0 bg-black bg-opacity-70 text-white font-bold px-2.5 py-1 rounded-md text-xs transition-opacity duration-200`}
>
⌘ + m
</div>
) : null}
</div>

{isChatMode ? (
Expand All @@ -83,10 +147,13 @@ export default function ChatInput({
<Mic className="w-4 h-4 text-[#999] dark:text-[#999]" />
</button>
) : null}

{isChatMode && curChatEnd ? (
<button
className={`ml-1 p-1 ${
inputValue ? "bg-[#0072FF]" : "bg-[#E4E5F0] dark:bg-[#545454]"
inputValue
? "bg-[#0072FF]"
: "bg-[#E4E5F0] dark:bg-[rgb(84,84,84)]"
} rounded-full transition-colors`}
type="submit"
onClick={() => onSend(inputValue.trim())}
Expand Down Expand Up @@ -116,38 +183,75 @@ export default function ChatInput({
{isChatMode ? (
<div className="flex gap-1 text-xs text-[#333] dark:text-[#d8d8d8]">
<button
className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors "
className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors relative"
onClick={openChatAI}
>
<Library className="w-4 h-4 mr-1 text-[#000] dark:text-[#d8d8d8]" />
Coco
{isCommandPressed ? (
<div
className={`absolute left-0 bg-black bg-opacity-70 text-white font-bold px-2.5 py-1 rounded-md text-xs transition-opacity duration-200`}
>
⌘ + o
</div>
) : null}
</button>
<button className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-color">
<button className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-color relative">
<Plus className="w-4 h-4 mr-1 text-[#000] dark:text-[#d8d8d8]" />
Upload
{isCommandPressed ? (
<div
className={`absolute left-1 bg-black bg-opacity-70 text-white font-bold px-2.5 py-1 rounded-md text-xs transition-opacity duration-200`}
>
⌘ + u
</div>
) : null}
</button>
</div>
) : (
<div className="flex gap-1">
<div className="w-28 flex gap-1 relative">
<button
className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors "
className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors relative"
onClick={openChatAI}
>
<AudioLines className="w-4 h-4 text-[#000] dark:text-[#d8d8d8]" />
</button>
<button className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-color">
<button className="inline-flex items-center p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-color relative">
<Image className="w-4 h-4 text-[#000] dark:text-[#d8d8d8]" />
</button>
{isCommandPressed ? (
<div
className={`absolute left-0 bg-black bg-opacity-70 text-white font-bold px-2.5 py-1 rounded-md text-xs transition-opacity duration-200`}
>
⌘ + n
</div>
) : null}
{isCommandPressed ? (
<div
className={`absolute left-14 bg-black bg-opacity-70 text-white font-bold px-2.5 py-1 rounded-md text-xs transition-opacity duration-200`}
>
⌘ + g
</div>
) : null}
</div>
)}

<ChatSwitch
isChatMode={isChatMode}
onChange={(value) => {
value && disabledChange();
changeMode(value);
}}
/>
<div className="relative w-24 flex justify-end items-center">
{isCommandPressed ? (
<div
className={`absolute left-0 z-10 bg-black bg-opacity-70 text-white font-bold px-2.5 py-1 rounded-md text-xs transition-opacity duration-200`}
>
⌘ + t
</div>
) : null}
<ChatSwitch
isChatMode={isChatMode}
onChange={(value) => {
value && disabledChange();
changeMode(value);
}}
/>
</div>
</div>
</div>
</div>
Expand Down
21 changes: 12 additions & 9 deletions src/components/Settings/GeneralSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import SettingsSelect from "./SettingsSelect";
import SettingsToggle from "./SettingsToggle";
import { ThemeOption } from "./index2";
import { type Hotkey } from "../../utils/tauri";
import { useTheme } from '../../contexts/ThemeContext';
import { useTheme } from "../../contexts/ThemeContext";

export default function GeneralSettings() {
const { theme, changeTheme } = useTheme();
Expand Down Expand Up @@ -104,7 +104,7 @@ export default function GeneralSettings() {
}

useEffect(() => {
getCurrentShortcut()
getCurrentShortcut();
}, []);

useEffect(() => {
Expand Down Expand Up @@ -190,7 +190,7 @@ export default function GeneralSettings() {

const handleStartListening = () => {
setPressedKeys(new Set());
setHotkey(null);
// setHotkey(null);
setListening(true);
};

Expand Down Expand Up @@ -220,12 +220,15 @@ export default function GeneralSettings() {
title="Coco Hotkey"
description="Global shortcut to open Coco"
>
<button
onClick={handleStartListening}
className="px-3 py-1.5 text-sm bg-gray-100 dark:bg-gray-700 rounded-md border border-gray-200 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors duration-200"
>
{listening ? "Listening..." : formatHotkey(hotkey)}
</button>
<div className="flex items-center gap-2">
<span className="text-blue-500">{listening ? "Listening..." : ""}</span>
<button
onClick={handleStartListening}
className="px-3 py-1.5 text-sm bg-gray-100 dark:bg-gray-700 rounded-md border border-gray-200 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors duration-200"
>
{formatHotkey(hotkey)}
</button>
</div>
</SettingsItem>

{/* <SettingsItem
Expand Down
2 changes: 1 addition & 1 deletion src/components/Settings/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface SettingsPanelProps {

const SettingsPanel: React.FC<SettingsPanelProps> = ({ children }) => {
return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6">
{/* <h2 className="text-xl font-semibold mb-6">{title}</h2> */}
{children}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function AppSettings() {
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 py-8">
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 flex min-h-[500px]">
<div className="bg-white rounded-2xl border border-gray-200 flex min-h-[500px]">
<div className="w-64 p-4 border-r border-gray-100">
<div className="space-y-1">
{sections.map((section) => (
Expand Down
Loading

0 comments on commit dd2260a

Please # to comment.