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:
CREDO23 2026-03-31 18:18:24 +02:00
parent 11387268a7
commit 95620a4331
3 changed files with 44 additions and 172 deletions

View file

@ -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"

View file

@ -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"

View file

@ -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(),
});