Skip to content

Commit

Permalink
feat: autocomplete app and window
Browse files Browse the repository at this point in the history
  • Loading branch information
louis030195 committed Sep 20, 2024
1 parent e6ea4de commit 0ec8f9d
Show file tree
Hide file tree
Showing 10 changed files with 406 additions and 106 deletions.
6 changes: 3 additions & 3 deletions content/docs/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}
64 changes: 64 additions & 0 deletions screenpipe-app-tauri/components/app-window-auto-complete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { useState, useEffect } from "react";
import { Input } from "@/components/ui/input";
import { useAppWindowHistory } from "@/lib/hooks/use-sql-autocomplete";
import { Command } from "cmdk";

interface AppWindowAutocompleteProps {
id: string;
placeholder: string;
value: string;
onChange: (value: string) => void;
type: "app" | "window";
icon: React.ReactNode;
}

export function AppWindowAutocomplete({
id,
placeholder,
value,
onChange,
type,
icon,
}: AppWindowAutocompleteProps) {
const { history, isLoading, error } = useAppWindowHistory(type);
const [open, setOpen] = useState(false);

if (error) {
console.error("error fetching history:", error);
}

return (
<div className="relative">
{icon}
<Command>
<Input
id={id}
type="text"
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
className="pl-8"
/>
{open && !isLoading && (
<Command.List className="absolute z-10 w-full bg-white border border-gray-300 rounded-md mt-1 max-h-60 overflow-auto">
<Command.Input />
{history.map((item) => (
<Command.Item
key={item.name}
value={item.name}
onSelect={(selectedValue) => {
onChange(selectedValue);
setOpen(false);
}}
>
{item.name} ({item.count})
</Command.Item>
))}
</Command.List>
)}
</Command>
</div>
);
}
114 changes: 42 additions & 72 deletions screenpipe-app-tauri/components/search-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ import { Checkbox } from "@/components/ui/checkbox";
import { formatISO } from "date-fns";
import { IconCode } from "@/components/ui/icons";
import { CodeBlock } from "./ui/codeblock";
import { SqlAutocompleteInput } from "./sql-autocomplete-input";
import { encode } from "@/lib/utils";

export function SearchChat() {
// Search state
Expand Down Expand Up @@ -124,23 +126,26 @@ export function SearchChat() {

const generateCurlCommand = () => {
const baseUrl = "http://localhost:3030";
const queryParams = new URLSearchParams({
const params = {
content_type: contentType,
limit: limit.toString(),
offset: offset.toString(),
start_time: formatISO(startDate, { representation: "complete" }),
end_time: formatISO(endDate, { representation: "complete" }),
min_length: minLength.toString(),
max_length: maxLength.toString(),
});
q: query,
app_name: appName,
window_name: windowName,
include_frames: includeFrames ? "true" : undefined,
};

if (query) queryParams.append("q", query);
if (appName) queryParams.append("app_name", appName);
if (windowName) queryParams.append("window_name", windowName);
if (includeFrames) queryParams.append("include_frames", "true");
const queryString = Object.entries(params)
.filter(([_, value]) => value !== undefined && value !== "")
.map(([key, value]) => ` ${key}=${JSON.stringify(value)}`)
.join(" \\\n");

return `curl "${baseUrl}/search?\\
${queryParams.toString().replace(/&/g, "\\\n&")}" | jq`;
return `curl "${baseUrl}/search" \\\n${queryString} | jq`;
};

useEffect(() => {
Expand Down Expand Up @@ -706,71 +711,34 @@ ${queryParams.toString().replace(/&/g, "\\\n&")}" | jq`;
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Label htmlFor="app-name">app name</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="h-4 w-4 text-gray-400" />
</TooltipTrigger>
<TooltipContent>
<p>
enter the name of the app to search for content for example
zoom, notion, etc. only works for ocr.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="relative">
<Laptop
className="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
<Input
id="app-name"
type="text"
placeholder="app name"
value={appName}
onChange={(e) => setAppName(e.target.value)}
autoCorrect="off"
className="pl-8"
/>
</div>
<SqlAutocompleteInput
id="app-name"
placeholder="app name"
value={appName}
onChange={setAppName}
type="app"
icon={
<Laptop
className="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
}
/>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Label htmlFor="window-name">window name</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="h-4 w-4 text-gray-400" />
</TooltipTrigger>
<TooltipContent>
<p>
enter the name of the window or tab to search for content.
can be a browser tab name, app tab name, etc. only works for
ocr.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="relative">
<Layout
className="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
<Input
id="window-name"
type="text"
placeholder="window name"
value={windowName}
onChange={(e) => setWindowName(e.target.value)}
autoCorrect="off"
className="pl-8"
/>
</div>
<SqlAutocompleteInput
id="window-name"
placeholder="window name"
value={windowName}
onChange={setWindowName}
type="window"
icon={
<Layout
className="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
}
/>
</div>
<div className="flex items-center space-x-2">
<Switch
Expand Down Expand Up @@ -916,7 +884,9 @@ ${queryParams.toString().replace(/&/g, "\\\n&")}" | jq`;
</span>
</DialogDescription>
</DialogHeader>
<CodeBlock language="bash" value={generateCurlCommand()} />
<div className="overflow-x-auto">
<CodeBlock language="bash" value={generateCurlCommand()} />
</div>
</DialogContent>
</Dialog>
</div>
Expand Down
137 changes: 137 additions & 0 deletions screenpipe-app-tauri/components/sql-autocomplete-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React, { useState, useRef, useCallback, useEffect } from "react";
import { useSqlAutocomplete } from "@/lib/hooks/use-sql-autocomplete";
import { Command } from "cmdk";
import { Input } from "@/components/ui/input";
import { Loader2, Search, X } from "lucide-react";

interface SqlAutocompleteInputProps {
id: string;
placeholder: string;
value: string;
onChange: (value: string) => void;
type: "app" | "window";
icon: React.ReactNode;
}

export function SqlAutocompleteInput({
id,
placeholder,
value,
onChange,
type,
icon,
}: SqlAutocompleteInputProps) {
const { items, isLoading } = useSqlAutocomplete(type);
const [open, setOpen] = useState(false);
const [inputValue, setInputValue] = useState(value);
const inputRef = useRef<HTMLInputElement>(null);
const commandRef = useRef<HTMLDivElement>(null);

const handleSelect = (selectedValue: string) => {
onChange(selectedValue);
setInputValue(selectedValue);
setOpen(false);
inputRef.current?.focus();
};

const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
onChange(e.target.value);
},
[onChange]
);

const handleClearInput = useCallback(() => {
setInputValue("");
onChange("");
inputRef.current?.focus();
}, [onChange]);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
commandRef.current &&
!commandRef.current.contains(event.target as Node)
) {
setOpen(false);
}
};

document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);

return (
<div className="relative" ref={commandRef}>
<div className="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400 z-10 flex items-center">
{icon}
<span className="w-2" />
</div>
<Command className="relative w-full" shouldFilter={false}>
<div className="relative">
<Input
ref={inputRef}
id={id}
type="text"
placeholder={placeholder}
value={inputValue}
onChange={handleInputChange}
onFocus={() => setOpen(true)}
className="pl-10 pr-8 w-full"
autoCorrect="off"
/>
{inputValue && (
<button
onClick={handleClearInput}
className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
<X className="h-4 w-4" />
</button>
)}
</div>
{open && (
<Command.List className="absolute z-20 w-full bg-white border border-gray-300 rounded-md mt-1 max-h-60 overflow-auto shadow-lg text-sm">
<div className="flex items-center px-3 py-2 border-b border-gray-200">
<Search className="mr-2 h-4 w-4 text-gray-400" />
<Command.Input
placeholder="search..."
value={inputValue}
onValueChange={setInputValue}
className="border-none focus:ring-0 outline-none w-full"
/>
</div>
{isLoading ? (
<Command.Loading>
<div className="px-4 py-2 text-gray-500 flex items-center">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
loading...
</div>
</Command.Loading>
) : (
items
.filter((item) =>
item.name.toLowerCase().includes(inputValue.toLowerCase())
)
.map((item: any) => (
<Command.Item
key={item.name}
value={item.name}
onSelect={handleSelect}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer border-b border-gray-200 last:border-b-0"
>
{item.name} ({item.count})
</Command.Item>
))
)}
{!isLoading && items.length === 0 && (
<div className="px-4 py-2 text-gray-500">no results found</div>
)}
</Command.List>
)}
</Command>
</div>
);
}
Loading

0 comments on commit 0ec8f9d

Please sign in to comment.