Improved tenant handling for slack bot (#4099)

This commit is contained in:
pablonyx
2025-02-26 20:06:26 -08:00
committed by GitHub
parent 2f64031f5c
commit 338e084062
6 changed files with 311 additions and 125 deletions

View File

@@ -1,8 +1,8 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useCallback } from "react";
import { InputPrompt } from "@/app/chat/interfaces";
import { Button } from "@/components/ui/button";
import { TrashIcon, PlusIcon } from "@/components/icons/icons";
import { MoreVertical, CheckIcon, XIcon } from "lucide-react";
import { PlusIcon } from "@/components/icons/icons";
import { MoreVertical, XIcon } from "lucide-react";
import { Textarea } from "@/components/ui/textarea";
import Title from "@/components/ui/title";
import Text from "@/components/ui/text";
@@ -153,114 +153,6 @@ export default function InputPrompts() {
}
};
const PromptCard = ({ prompt }: { prompt: InputPrompt }) => {
const isEditing = editingPromptId === prompt.id;
const [localPrompt, setLocalPrompt] = useState(prompt.prompt);
const [localContent, setLocalContent] = useState(prompt.content);
// Sync local edits with any prompt changes from outside
useEffect(() => {
setLocalPrompt(prompt.prompt);
setLocalContent(prompt.content);
}, [prompt, isEditing]);
const handleLocalEdit = (field: "prompt" | "content", value: string) => {
if (field === "prompt") {
setLocalPrompt(value);
} else {
setLocalContent(value);
}
};
const handleSaveLocal = () => {
handleSave(prompt.id, localPrompt, localContent);
};
return (
<div className="border dark:border-none dark:bg-[#333333] rounded-lg p-4 mb-4 relative">
{isEditing ? (
<>
<div className="absolute top-2 right-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
setEditingPromptId(null);
fetchInputPrompts(); // Revert changes from server
}}
>
<XIcon size={14} />
</Button>
</div>
<div className="flex">
<div className="flex-grow mr-4">
<Textarea
value={localPrompt}
onChange={(e) => handleLocalEdit("prompt", e.target.value)}
className="mb-2 resize-none"
placeholder="Prompt"
/>
<Textarea
value={localContent}
onChange={(e) => handleLocalEdit("content", e.target.value)}
className="resize-vertical min-h-[100px]"
placeholder="Content"
/>
</div>
<div className="flex items-end">
<Button onClick={handleSaveLocal}>
{prompt.id ? "Save" : "Create"}
</Button>
</div>
</div>
</>
) : (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="mb-2 flex gap-x-2 ">
<p className="font-semibold">{prompt.prompt}</p>
{isPromptPublic(prompt) && <SourceChip title="Built-in" />}
</div>
</TooltipTrigger>
{isPromptPublic(prompt) && (
<TooltipContent>
<p>This is a built-in prompt and cannot be edited</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<div className="whitespace-pre-wrap">{prompt.content}</div>
<div className="absolute top-2 right-2">
<DropdownMenu>
<DropdownMenuTrigger className="hover:bg-transparent" asChild>
<Button
className="!hover:bg-transparent"
variant="ghost"
size="sm"
>
<MoreVertical size={14} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{!isPromptPublic(prompt) && (
<DropdownMenuItem onClick={() => handleEdit(prompt.id)}>
Edit
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => handleDelete(prompt.id)}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</>
)}
</div>
);
};
return (
<div className="mx-auto max-w-4xl">
<div className="absolute top-4 left-4">
@@ -272,13 +164,21 @@ export default function InputPrompts() {
<Title>Prompt Shortcuts</Title>
<Text>
Manage and customize prompt shortcuts for your assistants. Use your
prompt shortcuts by starting a new message / in chat.
prompt shortcuts by starting a new message with &quot;/&quot; in
chat.
</Text>
</div>
</div>
{inputPrompts.map((prompt) => (
<PromptCard key={prompt.id} prompt={prompt} />
<PromptCard
key={prompt.id}
prompt={prompt}
onEdit={handleEdit}
onSave={handleSave}
onDelete={handleDelete}
isEditing={editingPromptId === prompt.id}
/>
))}
{isCreatingNew ? (
@@ -315,3 +215,129 @@ export default function InputPrompts() {
</div>
);
}
interface PromptCardProps {
prompt: InputPrompt;
onEdit: (id: number) => void;
onSave: (id: number, prompt: string, content: string) => void;
onDelete: (id: number) => void;
isEditing: boolean;
}
const PromptCard: React.FC<PromptCardProps> = ({
prompt,
onEdit,
onSave,
onDelete,
isEditing,
}) => {
const [localPrompt, setLocalPrompt] = useState(prompt.prompt);
const [localContent, setLocalContent] = useState(prompt.content);
useEffect(() => {
setLocalPrompt(prompt.prompt);
setLocalContent(prompt.content);
}, [prompt, isEditing]);
const handleLocalEdit = useCallback(
(field: "prompt" | "content", value: string) => {
if (field === "prompt") {
setLocalPrompt(value);
} else {
setLocalContent(value);
}
},
[]
);
const handleSaveLocal = useCallback(() => {
onSave(prompt.id, localPrompt, localContent);
}, [prompt.id, localPrompt, localContent, onSave]);
const isPromptPublic = useCallback((p: InputPrompt): boolean => {
return p.is_public;
}, []);
return (
<div className="border dark:border-none dark:bg-[#333333] rounded-lg p-4 mb-4 relative">
{isEditing ? (
<>
<div className="absolute top-2 right-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
onEdit(0);
}}
>
<XIcon size={14} />
</Button>
</div>
<div className="flex">
<div className="flex-grow mr-4">
<Textarea
value={localPrompt}
onChange={(e) => handleLocalEdit("prompt", e.target.value)}
className="mb-2 resize-none"
placeholder="Prompt"
/>
<Textarea
value={localContent}
onChange={(e) => handleLocalEdit("content", e.target.value)}
className="resize-vertical min-h-[100px]"
placeholder="Content"
/>
</div>
<div className="flex items-end">
<Button onClick={handleSaveLocal}>
{prompt.id ? "Save" : "Create"}
</Button>
</div>
</div>
</>
) : (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="mb-2 flex gap-x-2 ">
<p className="font-semibold">{prompt.prompt}</p>
{isPromptPublic(prompt) && <SourceChip title="Built-in" />}
</div>
</TooltipTrigger>
{isPromptPublic(prompt) && (
<TooltipContent>
<p>This is a built-in prompt and cannot be edited</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<div className="whitespace-pre-wrap">{prompt.content}</div>
<div className="absolute top-2 right-2">
<DropdownMenu>
<DropdownMenuTrigger className="hover:bg-transparent" asChild>
<Button
className="!hover:bg-transparent"
variant="ghost"
size="sm"
>
<MoreVertical size={14} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{!isPromptPublic(prompt) && (
<DropdownMenuItem onClick={() => onEdit(prompt.id)}>
Edit
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => onDelete(prompt.id)}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</>
)}
</div>
);
};

View File

@@ -0,0 +1,147 @@
import { SourceChip } from "../input/ChatInputBar";
import { useEffect } from "react";
import { useState } from "react";
import { InputPrompt } from "../interfaces";
import { Button } from "@/components/ui/button";
import { XIcon } from "@/components/icons/icons";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MoreVertical } from "lucide-react";
export const PromptCard = ({
prompt,
editingPromptId,
setEditingPromptId,
handleSave,
handleDelete,
isPromptPublic,
handleEdit,
fetchInputPrompts,
}: {
prompt: InputPrompt;
editingPromptId: number | null;
setEditingPromptId: (id: number | null) => void;
handleSave: (id: number, prompt: string, content: string) => void;
handleDelete: (id: number) => void;
isPromptPublic: (prompt: InputPrompt) => boolean;
handleEdit: (id: number) => void;
fetchInputPrompts: () => void;
}) => {
const isEditing = editingPromptId === prompt.id;
const [localPrompt, setLocalPrompt] = useState(prompt.prompt);
const [localContent, setLocalContent] = useState(prompt.content);
// Sync local edits with any prompt changes from outside
useEffect(() => {
setLocalPrompt(prompt.prompt);
setLocalContent(prompt.content);
}, [prompt, isEditing]);
const handleLocalEdit = (field: "prompt" | "content", value: string) => {
if (field === "prompt") {
setLocalPrompt(value);
} else {
setLocalContent(value);
}
};
const handleSaveLocal = () => {
handleSave(prompt.id, localPrompt, localContent);
};
return (
<div className="border dark:border-none dark:bg-[#333333] rounded-lg p-4 mb-4 relative">
{isEditing ? (
<>
<div className="absolute top-2 right-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
setEditingPromptId(null);
fetchInputPrompts(); // Revert changes from server
}}
>
<XIcon size={14} />
</Button>
</div>
<div className="flex">
<div className="flex-grow mr-4">
<Textarea
value={localPrompt}
onChange={(e) => handleLocalEdit("prompt", e.target.value)}
className="mb-2 resize-none"
placeholder="Prompt"
/>
<Textarea
value={localContent}
onChange={(e) => handleLocalEdit("content", e.target.value)}
className="resize-vertical min-h-[100px]"
placeholder="Content"
/>
</div>
<div className="flex items-end">
<Button onClick={handleSaveLocal}>
{prompt.id ? "Save" : "Create"}
</Button>
</div>
</div>
</>
) : (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="mb-2 flex gap-x-2 ">
<p className="font-semibold">{prompt.prompt}</p>
{isPromptPublic(prompt) && <SourceChip title="Built-in" />}
</div>
</TooltipTrigger>
{isPromptPublic(prompt) && (
<TooltipContent>
<p>This is a built-in prompt and cannot be edited</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<div className="whitespace-pre-wrap">{prompt.content}</div>
<div className="absolute top-2 right-2">
<DropdownMenu>
<DropdownMenuTrigger className="hover:bg-transparent" asChild>
<Button
className="!hover:bg-transparent"
variant="ghost"
size="sm"
>
<MoreVertical size={14} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{!isPromptPublic(prompt) && (
<DropdownMenuItem onClick={() => handleEdit(prompt.id)}>
Edit
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => handleDelete(prompt.id)}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</>
)}
</div>
);
};

View File

@@ -154,7 +154,6 @@ export const SourceChip = ({
gap-x-1
h-6
${onClick ? "cursor-pointer" : ""}
animate-fade-in-scale
`}
>
{icon}

View File

@@ -40,8 +40,13 @@ export function UserSettingsModal({
onClose: () => void;
defaultModel: string | null;
}) {
const { refreshUser, user, updateUserAutoScroll, updateUserShortcuts } =
useUser();
const {
refreshUser,
user,
updateUserAutoScroll,
updateUserShortcuts,
updateUserTemperatureOverrideEnabled,
} = useUser();
const containerRef = useRef<HTMLDivElement>(null);
const messageRef = useRef<HTMLDivElement>(null);
const { theme, setTheme } = useTheme();
@@ -156,11 +161,6 @@ export function UserSettingsModal({
const settings = useContext(SettingsContext);
const autoScroll = settings?.settings?.auto_scroll;
const checked =
user?.preferences?.auto_scroll === null
? autoScroll
: user?.preferences?.auto_scroll;
const handleChangePassword = async (e: React.FormEvent) => {
e.preventDefault();
if (newPassword !== confirmPassword) {
@@ -288,12 +288,26 @@ export function UserSettingsModal({
<SubLabel>Automatically scroll to new content</SubLabel>
</div>
<Switch
checked={checked}
checked={user?.preferences.auto_scroll}
onCheckedChange={(checked) => {
updateUserAutoScroll(checked);
}}
/>
</div>
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium">
Temperature override
</h3>
<SubLabel>Set the temperature for the LLM</SubLabel>
</div>
<Switch
checked={user?.preferences.temperature_override_enabled}
onCheckedChange={(checked) => {
updateUserTemperatureOverrideEnabled(checked);
}}
/>
</div>
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium">Prompt Shortcuts</h3>

View File

@@ -13,7 +13,7 @@ interface UserContextType {
isCurator: boolean;
refreshUser: () => Promise<void>;
isCloudSuperuser: boolean;
updateUserAutoScroll: (autoScroll: boolean | null) => Promise<void>;
updateUserAutoScroll: (autoScroll: boolean) => Promise<void>;
updateUserShortcuts: (enabled: boolean) => Promise<void>;
toggleAssistantPinnedStatus: (
currentPinnedAssistantIDs: number[],
@@ -163,7 +163,7 @@ export function UserProvider({
}
};
const updateUserAutoScroll = async (autoScroll: boolean | null) => {
const updateUserAutoScroll = async (autoScroll: boolean) => {
try {
const response = await fetch("/api/auto-scroll", {
method: "PATCH",

View File

@@ -10,7 +10,7 @@ interface UserPreferences {
pinned_assistants?: number[];
default_model: string | null;
recent_assistants: number[];
auto_scroll: boolean | null;
auto_scroll: boolean;
shortcut_enabled: boolean;
temperature_override_enabled: boolean;
}