mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-18 20:39:25 +01:00
Compare commits
6 Commits
main
...
add-search
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5df1f9668d | ||
|
|
23607e6b4c | ||
|
|
494be3db62 | ||
|
|
530502ecff | ||
|
|
8d6204a9d8 | ||
|
|
ca350dfeed |
@@ -48,6 +48,7 @@
|
||||
"@tauri-store/zustand": "^1.1.0",
|
||||
"@wavesurfer/react": "^1.0.11",
|
||||
"ahooks": "^3.8.4",
|
||||
"antd": "^6.1.1",
|
||||
"axios": "^1.12.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -57,7 +58,7 @@
|
||||
"i18next": "^23.16.8",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.461.0",
|
||||
"lucide-react": "^0.561.0",
|
||||
"mdast-util-gfm-autolink-literal": "2.0.0",
|
||||
"mermaid": "^11.6.0",
|
||||
"nanoid": "^5.1.5",
|
||||
|
||||
851
pnpm-lock.yaml
generated
851
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,8 @@ pub async fn query_coco_fusion(
|
||||
query_strings: HashMap<String, String>,
|
||||
query_timeout: u64,
|
||||
) -> Result<MultiSourceQueryResponse, SearchError> {
|
||||
println!("DBG: querystrings {:?}", query_strings);
|
||||
|
||||
if query_strings.contains_key("datasource") && !query_strings.contains_key("querysource") {
|
||||
panic!("[querysource] has to be provided if [datasource] is set")
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ const ExtensionStore = ({
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<img src={icon} className="size-[20px]" />
|
||||
<img src={icon} className="size-5" />
|
||||
<span className="whitespace-nowrap">{name}</span>
|
||||
<span className="truncate text-[#999]">{description}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { Brain, Sparkles } from "lucide-react";
|
||||
import { Brain, RotateCcw, ScanSearch, Sparkles } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -12,11 +12,13 @@ import { useConnectStore } from "@/stores/connectStore";
|
||||
import VisibleKey from "@/components/Common/VisibleKey";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { DEFAULT_FUZZINESS, useSearchStore } from "@/stores/searchStore";
|
||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||
import { parseSearchQuery, SearchQuery } from "@/utils";
|
||||
import InputUpload from "./InputUpload";
|
||||
import Copyright from "../Common/Copyright";
|
||||
import TimeFilter from "./TimeFilter";
|
||||
import { Slider } from "../ui/slider";
|
||||
|
||||
interface InputControlsProps {
|
||||
isChatMode: boolean;
|
||||
@@ -161,7 +163,13 @@ const InputControls = ({
|
||||
return state.aiOverviewAssistant;
|
||||
});
|
||||
const aiOverviewShortcut = useShortcutsStore((state) => state.aiOverview);
|
||||
const { visibleExtensionStore } = useSearchStore();
|
||||
const {
|
||||
visibleExtensionStore,
|
||||
enabledFuzzyMatch,
|
||||
setEnabledFuzzyMatch,
|
||||
fuzziness,
|
||||
setFuzziness,
|
||||
} = useSearchStore();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -242,7 +250,10 @@ const InputControls = ({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div data-tauri-drag-region className="w-28 flex gap-2 relative">
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="w-28 flex gap-2 items-center relative"
|
||||
>
|
||||
{!disabledExtensions.includes("AIOverview") &&
|
||||
isTauri &&
|
||||
aiOverviewServer &&
|
||||
@@ -274,12 +285,64 @@ const InputControls = ({
|
||||
</VisibleKey>
|
||||
|
||||
<span
|
||||
className={clsx("text-xs", { hidden: !enabledAiOverview })}
|
||||
className={clsx("text-xs truncate", {
|
||||
hidden: !enabledAiOverview,
|
||||
})}
|
||||
>
|
||||
AI Overview
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
"inline-flex items-center gap-1 h-5 px-1 rounded-full hover:text-[#881c94]! cursor-pointer transition",
|
||||
[
|
||||
enabledFuzzyMatch
|
||||
? "text-[#881c94]"
|
||||
: "text-[#333] dark:text-[#d8d8d8]",
|
||||
],
|
||||
{
|
||||
"bg-[#881C94]/20 dark:bg-[#202126]": enabledFuzzyMatch,
|
||||
}
|
||||
)}
|
||||
onClick={() => {
|
||||
setEnabledFuzzyMatch(!enabledFuzzyMatch);
|
||||
}}
|
||||
>
|
||||
<ScanSearch className="size-3" />
|
||||
|
||||
{enabledFuzzyMatch && (
|
||||
<>
|
||||
<span className={clsx("text-xs truncate")}>Fuzzy Match</span>
|
||||
|
||||
<Slider
|
||||
value={[fuzziness]}
|
||||
max={5}
|
||||
className="w-20"
|
||||
onValueChange={(value) => {
|
||||
setFuzziness(value[0]);
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
|
||||
<RotateCcw
|
||||
className="size-3"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
setFuzziness(DEFAULT_FUZZINESS);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="inline-flex items-center gap-1 h-5 px-1 rounded-full hover:text-[#881c94]! cursor-pointer transition">
|
||||
<TimeFilter />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
180
src/components/Search/TimeFilter.tsx
Normal file
180
src/components/Search/TimeFilter.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { ListFilter, ChevronRight, BrushCleaning } from "lucide-react";
|
||||
import { DatePicker } from "antd";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverPortal,
|
||||
PopoverTrigger,
|
||||
} from "../ui/popover";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import MultiSelect from "../ui/multi-select";
|
||||
|
||||
const TimeFilter = () => {
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const { aggregateFilter, setAggregateFilter } = useSearchStore();
|
||||
|
||||
const dropdownMenuItems = [
|
||||
{
|
||||
key: "all-time",
|
||||
label: "All Time",
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
key: "7-day",
|
||||
label: "7 Day",
|
||||
value: 7,
|
||||
},
|
||||
{
|
||||
key: "90-day",
|
||||
label: "90 Day",
|
||||
value: 90,
|
||||
},
|
||||
{
|
||||
key: "1-year",
|
||||
label: "1 Year",
|
||||
value: 365,
|
||||
},
|
||||
{
|
||||
key: "more",
|
||||
label: "More",
|
||||
onClick: () => {
|
||||
setPopoverOpen(true);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const typeOptions = [
|
||||
{
|
||||
label: "Web Page",
|
||||
value: "web_page",
|
||||
},
|
||||
{
|
||||
label: "PDF",
|
||||
value: "pdf",
|
||||
},
|
||||
{
|
||||
label: "Images",
|
||||
value: "images",
|
||||
},
|
||||
];
|
||||
|
||||
const sourceOptions = [
|
||||
{
|
||||
label: "INFINI Gateway",
|
||||
value: "INFINI Gateway",
|
||||
},
|
||||
{
|
||||
label: "INFINI Labs Blog",
|
||||
value: "INFINI Labs Blog",
|
||||
},
|
||||
{
|
||||
label: "Coco 官网",
|
||||
value: "Coco 官网",
|
||||
},
|
||||
{
|
||||
label: "INFINI Labs 中文官网",
|
||||
value: "INFINI Labs 中文官网",
|
||||
},
|
||||
{
|
||||
label: "INFINI Labs blog",
|
||||
value: "INFINI Labs blog",
|
||||
},
|
||||
];
|
||||
|
||||
console.log("triggerRef", triggerRef.current);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div ref={triggerRef} className="relative">
|
||||
<ListFilter className="size-3" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent>
|
||||
{dropdownMenuItems.map((item) => {
|
||||
const { key, label, onClick } = item;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={key}
|
||||
className="flex justify-between"
|
||||
onClick={() => {
|
||||
if (onClick) {
|
||||
onClick();
|
||||
} else {
|
||||
console.log("key", key);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>{label}</span>
|
||||
|
||||
{key === "more" && (
|
||||
<ChevronRight className="size-4 text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<div />
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverPortal container={triggerRef.current}>
|
||||
<PopoverContent className="w-100 p-4 text-sm">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="font-bold">Filters</span>
|
||||
|
||||
<BrushCleaning className="size-4 text-[#6000FF]" />
|
||||
</div>
|
||||
|
||||
<div className="pt-4 pb-2 text-[#999]">Date range</div>
|
||||
<DatePicker.RangePicker />
|
||||
|
||||
<div className="pt-4 pb-2 text-[#999]">Type</div>
|
||||
<MultiSelect
|
||||
value={aggregateFilter.type ?? []}
|
||||
placeholder="Please select type"
|
||||
options={typeOptions}
|
||||
onChange={(value) => {
|
||||
setAggregateFilter({
|
||||
...aggregateFilter,
|
||||
type: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="pt-4 pb-2 text-[#999]">Source</div>
|
||||
<MultiSelect
|
||||
value={aggregateFilter.source ?? []}
|
||||
placeholder="Please select source"
|
||||
options={sourceOptions}
|
||||
onChange={(value) => {
|
||||
setAggregateFilter({
|
||||
...aggregateFilter,
|
||||
source: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeFilter;
|
||||
@@ -1,97 +1,92 @@
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import { FC, useState } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "./dropdown-menu";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
export interface MultiSelectProps {
|
||||
options: Option[];
|
||||
value: string[];
|
||||
onChange?: (next: string[]) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
export interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const MultiSelect: React.FC<MultiSelectProps> = ({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "",
|
||||
className,
|
||||
disabled,
|
||||
}) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const values = React.useMemo(() => new Set(value), [value]);
|
||||
export interface MultiSelectProps {
|
||||
value: string[];
|
||||
options: Option[];
|
||||
placeholder?: string;
|
||||
onChange?: (value: string[]) => void;
|
||||
}
|
||||
|
||||
const toggle = (v: string) => {
|
||||
const next = new Set(values);
|
||||
if (next.has(v)) next.delete(v);
|
||||
else next.add(v);
|
||||
onChange?.(Array.from(next));
|
||||
const MultiSelect: FC<MultiSelectProps> = (props) => {
|
||||
const { value, options, placeholder, onChange } = props;
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const renderTrigger = () => {
|
||||
if (value.length === 0) {
|
||||
return <div className="text-muted-foreground px-1">{placeholder}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{value.map((item) => (
|
||||
<div className="inline-flex items-center gap-1 h-5.5 px-2 bg-muted rounded-md text-muted-foreground">
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const display = React.useMemo(() => {
|
||||
if (values.size === 0) return placeholder;
|
||||
const labels = options
|
||||
.filter((o) => values.has(o.value))
|
||||
.map((o) => o.label);
|
||||
return labels.join(", ");
|
||||
}, [options, values, placeholder]);
|
||||
|
||||
return (
|
||||
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
|
||||
<PopoverPrimitive.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className={cn(values.size === 0 && "text-muted-foreground")}>{display}</span>
|
||||
<svg
|
||||
className="h-4 w-4 opacity-70"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M6 9l6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</PopoverPrimitive.Trigger>
|
||||
<PopoverPrimitive.Content
|
||||
sideOffset={4}
|
||||
className={cn(
|
||||
"z-50 w-(--radix-popover-trigger-width) min-w-[220px] rounded-md border border-input bg-popover p-2 text-popover-foreground shadow-md outline-none",
|
||||
"ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
)}
|
||||
>
|
||||
<div className="max-h-48 overflow-y-auto space-y-1">
|
||||
{options.map((opt) => {
|
||||
const checked = values.has(opt.value) ? "checked" : "unchecked";
|
||||
return (
|
||||
<label
|
||||
key={opt.value}
|
||||
className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
toggle(opt.value);
|
||||
}}
|
||||
>
|
||||
<Checkbox checked={checked === "checked"} className="h-4 w-4" />
|
||||
<span className="text-sm">{opt.label}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
<DropdownMenu onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="flex items-center justify-between border border-border min-h-8 rounded-lg p-1">
|
||||
{renderTrigger()}
|
||||
|
||||
<ChevronDown
|
||||
className={cn("size-4 min-w-4 text-muted-foreground transition", {
|
||||
"rotate-180": open,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</PopoverPrimitive.Content>
|
||||
</PopoverPrimitive.Root>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent>
|
||||
{options.map((item) => {
|
||||
const { label, value: itemValue } = item;
|
||||
|
||||
const included = value.includes(itemValue);
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className={"flex items-center justify-between gap-2"}
|
||||
key={itemValue}
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (included) {
|
||||
onChange?.(value.filter((item) => item !== itemValue));
|
||||
} else {
|
||||
onChange?.([...value, itemValue]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>{label}</span>
|
||||
|
||||
<Check
|
||||
className={cn("size-4 text-muted-foreground", {
|
||||
"opacity-0": !value.includes(itemValue),
|
||||
})}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiSelect;
|
||||
|
||||
@@ -14,9 +14,16 @@ export type ViewExtensionOpened = [
|
||||
// HTML file URL
|
||||
string,
|
||||
ExtensionPermission | null,
|
||||
ViewExtensionUISettings | null,
|
||||
ViewExtensionUISettings | null
|
||||
];
|
||||
|
||||
export interface AggregateFilter {
|
||||
update_time_start?: string;
|
||||
update_time_end?: string;
|
||||
type?: string[];
|
||||
source?: string[];
|
||||
}
|
||||
|
||||
export type ISearchStore = {
|
||||
sourceData: any;
|
||||
setSourceData: (sourceData: any) => void;
|
||||
@@ -64,8 +71,19 @@ export type ISearchStore = {
|
||||
// When we open a View extension, we set this to a non-null value.
|
||||
viewExtensionOpened?: ViewExtensionOpened;
|
||||
setViewExtensionOpened: (showViewExtension?: ViewExtensionOpened) => void;
|
||||
|
||||
enabledFuzzyMatch: boolean;
|
||||
setEnabledFuzzyMatch: (enabledFuzzyMatch: boolean) => void;
|
||||
|
||||
fuzziness: number;
|
||||
setFuzziness: (fuzziness: number) => void;
|
||||
|
||||
aggregateFilter: AggregateFilter;
|
||||
setAggregateFilter: (aggregateFilter: AggregateFilter) => void;
|
||||
};
|
||||
|
||||
export const DEFAULT_FUZZINESS = 5;
|
||||
|
||||
export const useSearchStore = create<ISearchStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
@@ -138,11 +156,24 @@ export const useSearchStore = create<ISearchStore>()(
|
||||
setViewExtensionOpened: (viewExtensionOpened) => {
|
||||
return set({ viewExtensionOpened });
|
||||
},
|
||||
enabledFuzzyMatch: false,
|
||||
setEnabledFuzzyMatch: (enabledFuzzyMatch) => {
|
||||
return set({ enabledFuzzyMatch });
|
||||
},
|
||||
fuzziness: DEFAULT_FUZZINESS,
|
||||
setFuzziness: (fuzziness) => {
|
||||
return set({ fuzziness });
|
||||
},
|
||||
aggregateFilter: {},
|
||||
setAggregateFilter: (aggregateFilter) => {
|
||||
return set({ aggregateFilter });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "search-store",
|
||||
partialize: (state) => ({
|
||||
sourceData: state.sourceData,
|
||||
fuzziness: state.fuzziness,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user