mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-26 01:06:23 +02:00
Some checks are pending
Build and Push Docker Images / tag_release (push) Waiting to run
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (backend, surfsense-backend) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (web, surfsense-web) (push) Blocked by required conditions
- Introduced a `ProcessingMode` enum to differentiate between basic and premium processing modes. - Updated `EtlRequest` to include a `processing_mode` field, defaulting to basic. - Enhanced ETL pipeline services to utilize the selected processing mode for Azure Document Intelligence and LlamaCloud parsing. - Modified various routes and services to handle processing mode, affecting document upload and indexing tasks. - Improved error handling and logging to include processing mode details. - Added tests to validate processing mode functionality and its impact on ETL operations.
200 lines
6.3 KiB
TypeScript
200 lines
6.3 KiB
TypeScript
"use client";
|
|
|
|
import { useAtomValue, useSetAtom } from "jotai";
|
|
import { Plus, Zap } from "lucide-react";
|
|
import {
|
|
forwardRef,
|
|
useCallback,
|
|
useDeferredValue,
|
|
useEffect,
|
|
useImperativeHandle,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
|
|
import { promptsAtom } from "@/atoms/prompts/prompts-query.atoms";
|
|
import { userSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export interface PromptPickerRef {
|
|
selectHighlighted: () => void;
|
|
moveUp: () => void;
|
|
moveDown: () => void;
|
|
}
|
|
|
|
interface PromptPickerProps {
|
|
onSelect: (action: { name: string; prompt: string; mode: "transform" | "explore" }) => void;
|
|
onDone: () => void;
|
|
externalSearch?: string;
|
|
}
|
|
|
|
export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(function PromptPicker(
|
|
{ onSelect, onDone, externalSearch = "" },
|
|
ref
|
|
) {
|
|
const setUserSettingsDialog = useSetAtom(userSettingsDialogAtom);
|
|
const { data: prompts, isLoading, isError } = useAtomValue(promptsAtom);
|
|
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
const shouldScrollRef = useRef(false);
|
|
const itemRefs = useRef<Map<number, HTMLButtonElement>>(new Map());
|
|
|
|
// Defer the search value so filtering is non-urgent and the input stays responsive
|
|
const deferredSearch = useDeferredValue(externalSearch);
|
|
|
|
const filtered = useMemo(() => {
|
|
const list = prompts ?? [];
|
|
if (!deferredSearch) return list;
|
|
return list.filter((a) => a.name.toLowerCase().includes(deferredSearch.toLowerCase()));
|
|
}, [prompts, deferredSearch]);
|
|
|
|
// Reset highlight when the deferred (filtered) search changes
|
|
const prevSearchRef = useRef(deferredSearch);
|
|
if (prevSearchRef.current !== deferredSearch) {
|
|
prevSearchRef.current = deferredSearch;
|
|
if (highlightedIndex !== 0) {
|
|
setHighlightedIndex(0);
|
|
}
|
|
}
|
|
|
|
const createPromptIndex = filtered.length;
|
|
const totalItems = filtered.length + 1;
|
|
|
|
const handleSelect = useCallback(
|
|
(index: number) => {
|
|
if (index === createPromptIndex) {
|
|
onDone();
|
|
setUserSettingsDialog({ open: true, initialTab: "prompts" });
|
|
return;
|
|
}
|
|
const action = filtered[index];
|
|
if (!action) return;
|
|
onSelect({ name: action.name, prompt: action.prompt, mode: action.mode });
|
|
},
|
|
[filtered, onSelect, createPromptIndex, onDone, setUserSettingsDialog]
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!shouldScrollRef.current) return;
|
|
shouldScrollRef.current = false;
|
|
|
|
const rafId = requestAnimationFrame(() => {
|
|
const item = itemRefs.current.get(highlightedIndex);
|
|
const container = scrollContainerRef.current;
|
|
if (item && container) {
|
|
const itemRect = item.getBoundingClientRect();
|
|
const containerRect = container.getBoundingClientRect();
|
|
if (itemRect.top < containerRect.top || itemRect.bottom > containerRect.bottom) {
|
|
item.scrollIntoView({ block: "nearest" });
|
|
}
|
|
}
|
|
});
|
|
|
|
return () => cancelAnimationFrame(rafId);
|
|
}, [highlightedIndex]);
|
|
|
|
useImperativeHandle(
|
|
ref,
|
|
() => ({
|
|
selectHighlighted: () => handleSelect(highlightedIndex),
|
|
moveUp: () => {
|
|
shouldScrollRef.current = true;
|
|
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : totalItems - 1));
|
|
},
|
|
moveDown: () => {
|
|
shouldScrollRef.current = true;
|
|
setHighlightedIndex((prev) => (prev < totalItems - 1 ? prev + 1 : 0));
|
|
},
|
|
}),
|
|
[totalItems, highlightedIndex, handleSelect]
|
|
);
|
|
|
|
return (
|
|
<div className="shadow-2xl rounded-lg border border-border dark:border-white/5 overflow-hidden bg-popover dark:bg-neutral-900 flex flex-col w-[280px] sm:w-[320px] select-none">
|
|
<div ref={scrollContainerRef} className="max-h-[180px] sm:max-h-[280px] overflow-y-auto">
|
|
{isLoading ? (
|
|
<div className="py-1 px-2">
|
|
<div className="px-3 py-2">
|
|
<Skeleton className="h-[16px] w-24" />
|
|
</div>
|
|
{["a", "b", "c", "d", "e"].map((id, i) => (
|
|
<div
|
|
key={id}
|
|
className={cn(
|
|
"w-full flex items-center gap-2 px-3 py-2 text-left rounded-md",
|
|
i >= 3 && "hidden sm:flex"
|
|
)}
|
|
>
|
|
<span className="shrink-0">
|
|
<Skeleton className="h-4 w-4" />
|
|
</span>
|
|
<span className="flex-1 text-sm">
|
|
<Skeleton className="h-[20px]" style={{ width: `${60 + ((i * 7) % 30)}%` }} />
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : isError ? (
|
|
<div className="py-1 px-2">
|
|
<p className="px-3 py-2 text-xs text-destructive">Failed to load prompts</p>
|
|
</div>
|
|
) : filtered.length === 0 ? (
|
|
<div className="py-1 px-2">
|
|
<p className="px-3 py-2 text-xs text-muted-foreground">No matching prompts</p>
|
|
</div>
|
|
) : (
|
|
<div className="py-1 px-2">
|
|
<div className="px-3 py-2 text-xs font-bold text-muted-foreground/55">
|
|
Saved Prompts
|
|
</div>
|
|
{filtered.map((action, index) => (
|
|
<button
|
|
key={action.id}
|
|
ref={(el) => {
|
|
if (el) itemRefs.current.set(index, el);
|
|
else itemRefs.current.delete(index);
|
|
}}
|
|
type="button"
|
|
onClick={() => handleSelect(index)}
|
|
onMouseEnter={() => setHighlightedIndex(index)}
|
|
className={cn(
|
|
"w-full flex items-center gap-2 px-3 py-2 text-left text-sm transition-colors rounded-md cursor-pointer",
|
|
index === highlightedIndex && "bg-accent"
|
|
)}
|
|
>
|
|
<span className="shrink-0 text-muted-foreground">
|
|
<Zap className="size-4" />
|
|
</span>
|
|
<span className="flex-1 text-sm truncate">{action.name}</span>
|
|
</button>
|
|
))}
|
|
|
|
<div className="mx-2 my-1 border-t border-border dark:border-white/5" />
|
|
<button
|
|
ref={(el) => {
|
|
if (el) itemRefs.current.set(createPromptIndex, el);
|
|
else itemRefs.current.delete(createPromptIndex);
|
|
}}
|
|
type="button"
|
|
onClick={() => handleSelect(createPromptIndex)}
|
|
onMouseEnter={() => setHighlightedIndex(createPromptIndex)}
|
|
className={cn(
|
|
"w-full flex items-center gap-2 px-3 py-2 text-left text-sm transition-colors rounded-md cursor-pointer text-muted-foreground",
|
|
highlightedIndex === createPromptIndex
|
|
? "bg-accent text-foreground"
|
|
: "hover:text-foreground hover:bg-accent/50"
|
|
)}
|
|
>
|
|
<span className="shrink-0">
|
|
<Plus className="size-4" />
|
|
</span>
|
|
<span>Create prompt</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|