mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
Unify frontend prompt rendering to use API-only prompt library
Remove hardcoded DEFAULT_ACTIONS and icon map from prompt-picker, fetch all prompts from the backend. Simplify Zod types to match the single-table schema (drop source/system_prompt_slug/is_modified, add version). Update PromptsContent empty state copy.
This commit is contained in:
parent
11387268a7
commit
95620a4331
3 changed files with 44 additions and 172 deletions
|
|
@ -173,32 +173,32 @@ export function PromptsContent() {
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="prompt-public"
|
||||
checked={formData.is_public}
|
||||
onCheckedChange={(checked) => setFormData((p) => ({ ...p, is_public: checked }))}
|
||||
/>
|
||||
<Label htmlFor="prompt-public" className="text-sm font-normal">
|
||||
Share with community
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="prompt-public"
|
||||
checked={formData.is_public}
|
||||
onCheckedChange={(checked) => setFormData((p) => ({ ...p, is_public: checked }))}
|
||||
/>
|
||||
<Label htmlFor="prompt-public" className="text-sm font-normal">
|
||||
Share with community
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 pt-2">
|
||||
<Button variant="ghost" size="sm" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? <Spinner className="size-3.5" /> : editingId ? "Update" : "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 pt-2">
|
||||
<Button variant="ghost" size="sm" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? <Spinner className="size-3.5" /> : editingId ? "Update" : "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{prompts.length === 0 && !showForm && (
|
||||
<div className="rounded-lg border border-dashed border-border/60 p-8 text-center">
|
||||
<Sparkles className="mx-auto size-8 text-muted-foreground/40" />
|
||||
<p className="mt-2 text-sm text-muted-foreground">No custom prompts yet</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">No prompts yet</p>
|
||||
<p className="text-xs text-muted-foreground/60">
|
||||
Create prompts to quickly transform or explore text with /
|
||||
</p>
|
||||
|
|
@ -228,7 +228,9 @@ export function PromptsContent() {
|
|||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className={`mt-1 text-xs text-muted-foreground ${expandedId === prompt.id ? "whitespace-pre-wrap" : "line-clamp-2"}`}>
|
||||
<p
|
||||
className={`mt-1 text-xs text-muted-foreground ${expandedId === prompt.id ? "whitespace-pre-wrap" : "line-clamp-2"}`}
|
||||
>
|
||||
{prompt.prompt}
|
||||
</p>
|
||||
{prompt.prompt.length > 100 && (
|
||||
|
|
@ -247,7 +249,9 @@ export function PromptsContent() {
|
|||
title={prompt.is_public ? "Make private" : "Share with community"}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const updated = await promptsApiService.update(prompt.id, { is_public: !prompt.is_public });
|
||||
const updated = await promptsApiService.update(prompt.id, {
|
||||
is_public: !prompt.is_public,
|
||||
});
|
||||
setPrompts((prev) => prev.map((p) => (p.id === prompt.id ? updated : p)));
|
||||
toast.success(updated.is_public ? "Shared with community" : "Made private");
|
||||
} catch {
|
||||
|
|
@ -256,7 +260,11 @@ export function PromptsContent() {
|
|||
}}
|
||||
className="flex items-center justify-center size-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||
>
|
||||
{prompt.is_public ? <Lock className="size-3.5" /> : <Globe className="size-3.5" />}
|
||||
{prompt.is_public ? (
|
||||
<Lock className="size-3.5" />
|
||||
) : (
|
||||
<Globe className="size-3.5" />
|
||||
)}
|
||||
</button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -1,18 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useSetAtom } from "jotai";
|
||||
import {
|
||||
BookOpen,
|
||||
Check,
|
||||
Globe,
|
||||
Languages,
|
||||
List,
|
||||
Minimize2,
|
||||
PenLine,
|
||||
Plus,
|
||||
Search,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { Plus, Zap } from "lucide-react";
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
|
|
@ -41,86 +30,13 @@ interface PromptPickerProps {
|
|||
containerStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const ICONS: Record<string, React.ReactNode> = {
|
||||
check: <Check className="size-3.5" />,
|
||||
minimize: <Minimize2 className="size-3.5" />,
|
||||
languages: <Languages className="size-3.5" />,
|
||||
"pen-line": <PenLine className="size-3.5" />,
|
||||
"book-open": <BookOpen className="size-3.5" />,
|
||||
list: <List className="size-3.5" />,
|
||||
search: <Search className="size-3.5" />,
|
||||
globe: <Globe className="size-3.5" />,
|
||||
zap: <Zap className="size-3.5" />,
|
||||
};
|
||||
|
||||
const DEFAULT_ACTIONS: {
|
||||
name: string;
|
||||
prompt: string;
|
||||
mode: "transform" | "explore";
|
||||
icon: string;
|
||||
}[] = [
|
||||
{
|
||||
name: "Fix grammar",
|
||||
prompt:
|
||||
"Fix the grammar and spelling in the following text. Return only the corrected text, nothing else.\n\n{selection}",
|
||||
mode: "transform",
|
||||
icon: "check",
|
||||
},
|
||||
{
|
||||
name: "Make shorter",
|
||||
prompt:
|
||||
"Make the following text more concise while preserving its meaning. Return only the shortened text, nothing else.\n\n{selection}",
|
||||
mode: "transform",
|
||||
icon: "minimize",
|
||||
},
|
||||
{
|
||||
name: "Translate",
|
||||
prompt:
|
||||
"Translate the following text to English. If it is already in English, translate it to French. Return only the translation, nothing else.\n\n{selection}",
|
||||
mode: "transform",
|
||||
icon: "languages",
|
||||
},
|
||||
{
|
||||
name: "Rewrite",
|
||||
prompt:
|
||||
"Rewrite the following text to improve clarity and readability. Return only the rewritten text, nothing else.\n\n{selection}",
|
||||
mode: "transform",
|
||||
icon: "pen-line",
|
||||
},
|
||||
{
|
||||
name: "Summarize",
|
||||
prompt:
|
||||
"Summarize the following text concisely. Return only the summary, nothing else.\n\n{selection}",
|
||||
mode: "transform",
|
||||
icon: "list",
|
||||
},
|
||||
{
|
||||
name: "Explain",
|
||||
prompt: "Explain the following text in simple terms:\n\n{selection}",
|
||||
mode: "explore",
|
||||
icon: "book-open",
|
||||
},
|
||||
{
|
||||
name: "Ask my knowledge base",
|
||||
prompt: "Search my knowledge base for information related to:\n\n{selection}",
|
||||
mode: "explore",
|
||||
icon: "search",
|
||||
},
|
||||
{
|
||||
name: "Look up on the web",
|
||||
prompt: "Search the web for information about:\n\n{selection}",
|
||||
mode: "explore",
|
||||
icon: "globe",
|
||||
},
|
||||
];
|
||||
|
||||
export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(function PromptPicker(
|
||||
{ onSelect, onDone, externalSearch = "", containerStyle },
|
||||
ref
|
||||
) {
|
||||
const setUserSettingsDialog = useSetAtom(userSettingsDialogAtom);
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
||||
const [customPrompts, setCustomPrompts] = useState<PromptRead[]>([]);
|
||||
const [prompts, setPrompts] = useState<PromptRead[]>([]);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const shouldScrollRef = useRef(false);
|
||||
const itemRefs = useRef<Map<number, HTMLButtonElement>>(new Map());
|
||||
|
|
@ -128,26 +44,15 @@ export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(funct
|
|||
useEffect(() => {
|
||||
promptsApiService
|
||||
.list()
|
||||
.then(setCustomPrompts)
|
||||
.then(setPrompts)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const allActions = useMemo(() => {
|
||||
const customs = customPrompts.map((a) => ({
|
||||
name: a.name,
|
||||
prompt: a.prompt,
|
||||
mode: a.mode as "transform" | "explore",
|
||||
icon: a.icon || "zap",
|
||||
}));
|
||||
return [...DEFAULT_ACTIONS, ...customs];
|
||||
}, [customPrompts]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!externalSearch) return allActions;
|
||||
return allActions.filter((a) => a.name.toLowerCase().includes(externalSearch.toLowerCase()));
|
||||
}, [allActions, externalSearch]);
|
||||
if (!externalSearch) return prompts;
|
||||
return prompts.filter((a) => a.name.toLowerCase().includes(externalSearch.toLowerCase()));
|
||||
}, [prompts, externalSearch]);
|
||||
|
||||
// Reset highlight when results change
|
||||
const prevSearchRef = useRef(externalSearch);
|
||||
if (prevSearchRef.current !== externalSearch) {
|
||||
prevSearchRef.current = externalSearch;
|
||||
|
|
@ -165,7 +70,6 @@ export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(funct
|
|||
[filtered, onSelect]
|
||||
);
|
||||
|
||||
// Auto-scroll highlighted item into view
|
||||
useEffect(() => {
|
||||
if (!shouldScrollRef.current) return;
|
||||
shouldScrollRef.current = false;
|
||||
|
|
@ -203,18 +107,15 @@ export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(funct
|
|||
|
||||
if (filtered.length === 0) return null;
|
||||
|
||||
const defaultFiltered = filtered.filter((_, i) => i < DEFAULT_ACTIONS.length);
|
||||
const customFiltered = filtered.filter((_, i) => i >= DEFAULT_ACTIONS.length);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-64 rounded-lg border bg-popover shadow-lg overflow-hidden"
|
||||
style={containerStyle}
|
||||
>
|
||||
<div ref={scrollContainerRef} className="max-h-48 overflow-y-auto py-1">
|
||||
{defaultFiltered.map((action, index) => (
|
||||
{filtered.map((action, index) => (
|
||||
<button
|
||||
key={action.name}
|
||||
key={action.id}
|
||||
ref={(el) => {
|
||||
if (el) itemRefs.current.set(index, el);
|
||||
else itemRefs.current.delete(index);
|
||||
|
|
@ -228,39 +129,12 @@ export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(funct
|
|||
)}
|
||||
>
|
||||
<span className="text-muted-foreground">
|
||||
{ICONS[action.icon] ?? <Zap className="size-3.5" />}
|
||||
<Zap className="size-3.5" />
|
||||
</span>
|
||||
<span className="truncate">{action.name}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{customFiltered.length > 0 && <div className="my-1 h-px bg-border mx-2" />}
|
||||
|
||||
{customFiltered.map((action, i) => {
|
||||
const index = defaultFiltered.length + i;
|
||||
return (
|
||||
<button
|
||||
key={action.name}
|
||||
ref={(el) => {
|
||||
if (el) itemRefs.current.set(index, el);
|
||||
else itemRefs.current.delete(index);
|
||||
}}
|
||||
type="button"
|
||||
onClick={() => handleSelect(index)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2 px-3 py-1.5 text-sm cursor-pointer",
|
||||
index === highlightedIndex ? "bg-accent" : "hover:bg-accent/50"
|
||||
)}
|
||||
>
|
||||
<span className="text-muted-foreground">
|
||||
<Zap className="size-3.5" />
|
||||
</span>
|
||||
<span className="truncate">{action.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="my-1 h-px bg-border mx-2" />
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -3,16 +3,14 @@ import { z } from "zod";
|
|||
export type PromptMode = "transform" | "explore";
|
||||
|
||||
export const promptRead = z.object({
|
||||
id: z.number().nullable(),
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
prompt: z.string(),
|
||||
mode: z.enum(["transform", "explore"]),
|
||||
search_space_id: z.number().nullable().optional(),
|
||||
is_public: z.boolean().optional(),
|
||||
created_at: z.string().nullable().optional(),
|
||||
source: z.enum(["system", "custom"]),
|
||||
system_prompt_slug: z.string().nullable().optional(),
|
||||
is_modified: z.boolean().optional(),
|
||||
search_space_id: z.number().nullable(),
|
||||
is_public: z.boolean(),
|
||||
version: z.number(),
|
||||
created_at: z.string(),
|
||||
});
|
||||
|
||||
export type PromptRead = z.infer<typeof promptRead>;
|
||||
|
|
@ -46,14 +44,6 @@ export const promptUpdateRequest = z.object({
|
|||
|
||||
export type PromptUpdateRequest = z.infer<typeof promptUpdateRequest>;
|
||||
|
||||
export const systemPromptUpdateRequest = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
prompt: z.string().min(1).optional(),
|
||||
mode: z.enum(["transform", "explore"]).optional(),
|
||||
});
|
||||
|
||||
export type SystemPromptUpdateRequest = z.infer<typeof systemPromptUpdateRequest>;
|
||||
|
||||
export const promptDeleteResponse = z.object({
|
||||
success: z.boolean(),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue