feat(ui): SearchResultsDisplay + PromptMessagesCard + LoggingCallbacksTable

- SearchResultsDisplay: antd Button + @ant-design Database/FileText/
  Chevrons \u2192 shadcn Button/Badge + lucide equivalents. Blue score
  chip added to categorical overrides.
- PromptMessagesCard: @tremor Card/Text + antd Select \u2192 shadcn Card
  + Select. Drag-and-drop reorder preserved (pure HTML5 drag
  handlers, no antd dependency).
- LoggingCallbacksTable: @tremor Button + antd Table/Title \u2192 shadcn
  Button + shadcn Table + plain h3. Categorical success/failure/info
  badges added to overrides.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 12:38:58 +00:00
parent db992fba74
commit fa9d5ae6d9
No known key found for this signature in database
4 changed files with 234 additions and 151 deletions

View file

@ -62,6 +62,8 @@
"src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.tsx",
"src/components/prompts/prompt_editor_view/conversation_panel/MessageBubble.tsx",
"src/components/playground/chat_ui/SessionManagement.tsx",
"src/components/playground/chat_ui/SearchResultsDisplay.tsx",
"src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.tsx",
"src/components/policies/impact_preview_alert.tsx",
"src/components/policies/impact_popover.tsx",
"src/components/DeletedKeysPage/DeletedKeysPage.tsx",

View file

@ -1,7 +1,13 @@
import { Button } from "@tremor/react";
import type { TableProps } from "antd";
import { Table } from "antd";
import Title from "antd/es/typography/Title";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import React from "react";
import TableIconActionButton from "../../../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
import { AlertingObject } from "./types";
@ -41,81 +47,96 @@ export const LoggingCallbacksTable: React.FC<LoggingCallbacksProps> = ({
onDelete = () => {},
onAdd = () => {},
}) => {
const columns: TableProps<CallbackRow>["columns"] = [
{
title: <span className="font-medium text-gray-700">Callback Name</span>,
dataIndex: "name",
key: "name",
render: (_: string, record: CallbackRow) => {
const id = record.name;
console.log("availableCallbacks", availableCallbacks);
const displayName = availableCallbacks[id]?.ui_callback_name || id;
return <div className="font-medium text-gray-800">{displayName}</div>;
},
},
{
title: <span className="font-medium text-gray-700">Mode</span>,
key: "mode",
render: (_: unknown, record: CallbackRow) => {
const mode = record.mode || "success";
const label = CALLBACK_MODES.find((m) => m.value === mode)?.label || mode;
const badgeClass =
mode === "success"
? "bg-green-100 text-green-800"
: mode === "failure"
? "bg-red-100 text-red-800"
: "bg-blue-100 text-blue-800";
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${badgeClass}`}>
{label}
</span>
);
},
width: 240,
},
{
title: <span className="font-medium text-gray-700 text-right w-full block">Actions</span>,
key: "actions",
align: "right",
render: (_: unknown, record: CallbackRow) => (
<div className="flex justify-end gap-2">
<TableIconActionButton variant="Test" tooltipText="Test Callback" onClick={() => onTest(record)} />
<TableIconActionButton variant="Edit" tooltipText="Edit Callback" onClick={() => onEdit(record)} />
<TableIconActionButton variant="Delete" tooltipText="Delete Callback" onClick={() => onDelete(record)} />
</div>
),
width: 240,
},
];
return (
<>
<div className="w-full mt-4">
<Button onClick={onAdd} className="mx-auto">
+ Add Callback
</Button>
<div className="flex justify-between items-center my-2">
<Title level={4}>Active Logging Callbacks</Title>
</div>
{/* Empty state */}
{callbacks.length === 0 ? (
<div className="flex flex-col items-center justify-center p-8 bg-gray-50 border border-gray-200 rounded-lg">
<div className="text-center">
<h3 className="text-lg font-medium text-gray-700 mb-2">No callbacks configured</h3>
<p className="text-gray-500">Add your first callback to start logging data to external services.</p>
</div>
</div>
) : (
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
<Table
columns={columns}
dataSource={callbacks as CallbackRow[]}
rowKey={(record) => record.name}
pagination={false}
rowClassName={() => "hover:bg-gray-50"}
/>
</div>
)}
<div className="w-full mt-4">
<Button onClick={onAdd} className="mx-auto">
+ Add Callback
</Button>
<div className="flex justify-between items-center my-2">
<h3 className="text-lg font-semibold">Active Logging Callbacks</h3>
</div>
</>
{callbacks.length === 0 ? (
<div className="flex flex-col items-center justify-center p-8 bg-muted border border-border rounded-lg">
<div className="text-center">
<h3 className="text-lg font-medium text-foreground mb-2">
No callbacks configured
</h3>
<p className="text-muted-foreground">
Add your first callback to start logging data to external
services.
</p>
</div>
</div>
) : (
<div className="bg-background border border-border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="font-medium">Callback Name</TableHead>
<TableHead className="font-medium w-[240px]">Mode</TableHead>
<TableHead className="font-medium text-right w-[240px]">
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{callbacks.map((cb) => {
const record = cb as CallbackRow;
const id = record.name;
const displayName =
availableCallbacks[id]?.ui_callback_name || id;
const mode = record.mode || "success";
const label =
CALLBACK_MODES.find((m) => m.value === mode)?.label || mode;
const badgeClass =
mode === "success"
? "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300"
: mode === "failure"
? "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300"
: "bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300";
return (
<TableRow key={record.name} className="hover:bg-muted">
<TableCell>
<div className="font-medium text-foreground">
{displayName}
</div>
</TableCell>
<TableCell>
<span
className={cn(
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
badgeClass,
)}
>
{label}
</span>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<TableIconActionButton
variant="Test"
tooltipText="Test Callback"
onClick={() => onTest(record)}
/>
<TableIconActionButton
variant="Edit"
tooltipText="Edit Callback"
onClick={() => onEdit(record)}
/>
<TableIconActionButton
variant="Delete"
tooltipText="Delete Callback"
onClick={() => onDelete(record)}
/>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
)}
</div>
);
};

View file

@ -1,15 +1,26 @@
import React, { useState } from "react";
import { Button } from "antd";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
ChevronDown,
ChevronRight,
Database,
FileText,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { VectorStoreSearchResponse } from "./types";
import { DatabaseOutlined, FileTextOutlined, DownOutlined, RightOutlined } from "@ant-design/icons";
interface SearchResultsDisplayProps {
searchResults: VectorStoreSearchResponse[];
}
export function SearchResultsDisplay({ searchResults }: SearchResultsDisplayProps) {
export function SearchResultsDisplay({
searchResults,
}: SearchResultsDisplayProps) {
const [isExpanded, setIsExpanded] = useState(true);
const [expandedResults, setExpandedResults] = useState<Record<string, boolean>>({});
const [expandedResults, setExpandedResults] = useState<
Record<string, boolean>
>({});
if (!searchResults || searchResults.length === 0) {
return null;
@ -23,85 +34,114 @@ export function SearchResultsDisplay({ searchResults }: SearchResultsDisplayProp
}));
};
const totalResults = searchResults.reduce((sum, page) => sum + page.data.length, 0);
const totalResults = searchResults.reduce(
(sum, page) => sum + page.data.length,
0,
);
return (
<div className="search-results-content mt-1 mb-2">
<Button
type="text"
className="flex items-center text-xs text-gray-500 hover:text-gray-700"
variant="ghost"
size="sm"
className="flex items-center text-xs text-muted-foreground hover:text-foreground"
onClick={() => setIsExpanded(!isExpanded)}
icon={<DatabaseOutlined />}
>
<Database className="h-3.5 w-3.5" />
{isExpanded ? "Hide sources" : `Show sources (${totalResults})`}
{isExpanded ? <DownOutlined className="ml-1" /> : <RightOutlined className="ml-1" />}
{isExpanded ? (
<ChevronDown className="h-3 w-3 ml-1" />
) : (
<ChevronRight className="h-3 w-3 ml-1" />
)}
</Button>
{isExpanded && (
<div className="mt-2 p-3 bg-gray-50 border border-gray-200 rounded-md text-sm">
<div className="mt-2 p-3 bg-muted border border-border rounded-md text-sm">
<div className="space-y-3">
{searchResults.map((resultPage, pageIndex) => (
<div key={pageIndex}>
<div className="text-xs text-gray-600 mb-2 flex items-center gap-2">
<div className="text-xs text-muted-foreground mb-2 flex items-center gap-2">
<span className="font-medium">Query:</span>
<span className="italic">&quot;{resultPage.search_query}&quot;</span>
<span className="text-gray-400">•</span>
<span className="text-gray-500">{resultPage.data.length} result{resultPage.data.length !== 1 ? 's' : ''}</span>
<span className="italic">
&quot;{resultPage.search_query}&quot;
</span>
<span>•</span>
<span>
{resultPage.data.length} result
{resultPage.data.length !== 1 ? "s" : ""}
</span>
</div>
<div className="space-y-2">
{resultPage.data.map((result, resultIndex) => {
const isResultExpanded = expandedResults[`${pageIndex}-${resultIndex}`] || false;
const isResultExpanded =
expandedResults[`${pageIndex}-${resultIndex}`] || false;
return (
<div key={resultIndex} className="border border-gray-200 rounded-md overflow-hidden bg-white">
<div
key={resultIndex}
className="border border-border rounded-md overflow-hidden bg-background"
>
<div
className="flex items-center justify-between p-2 cursor-pointer hover:bg-gray-50 transition-colors"
className="flex items-center justify-between p-2 cursor-pointer hover:bg-muted transition-colors"
onClick={() => toggleResult(pageIndex, resultIndex)}
>
<div className="flex items-center gap-2 flex-1 min-w-0">
<svg
className={`w-4 h-4 text-gray-400 transition-transform flex-shrink-0 ${isResultExpanded ? "transform rotate-90" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<FileTextOutlined className="text-gray-400 flex-shrink-0" style={{ fontSize: "12px" }} />
<span className="text-xs font-medium text-gray-700 truncate">
{result.filename || result.file_id || `Result ${resultIndex + 1}`}
<ChevronRight
className={cn(
"w-4 h-4 text-muted-foreground transition-transform flex-shrink-0",
isResultExpanded && "rotate-90",
)}
/>
<FileText className="w-3 h-3 text-muted-foreground flex-shrink-0" />
<span className="text-xs font-medium text-foreground truncate">
{result.filename ||
result.file_id ||
`Result ${resultIndex + 1}`}
</span>
<span className="text-xs px-2 py-0.5 rounded bg-blue-100 text-blue-700 font-mono flex-shrink-0">
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300 text-xs font-mono flex-shrink-0">
{result.score.toFixed(3)}
</span>
</Badge>
</div>
</div>
{isResultExpanded && (
<div className="border-t border-gray-200 bg-white">
<div className="border-t border-border bg-background">
<div className="p-3 space-y-2">
{result.content.map((content, contentIndex) => (
<div key={contentIndex}>
<div className="text-xs font-mono bg-gray-50 p-2 rounded text-gray-800 whitespace-pre-wrap break-words">
<div className="text-xs font-mono bg-muted p-2 rounded text-foreground whitespace-pre-wrap break-words">
{content.text}
</div>
</div>
))}
{result.attributes && Object.keys(result.attributes).length > 0 && (
<div className="mt-2 pt-2 border-t border-gray-100">
<div className="text-xs text-gray-500 mb-1 font-medium">Metadata:</div>
<div className="space-y-1">
{Object.entries(result.attributes).map(([key, value]) => (
<div key={key} className="text-xs flex gap-2">
<span className="text-gray-500 font-medium">{key}:</span>
<span className="text-gray-700 font-mono break-all">{String(value)}</span>
</div>
))}
{result.attributes &&
Object.keys(result.attributes).length > 0 && (
<div className="mt-2 pt-2 border-t border-border">
<div className="text-xs text-muted-foreground mb-1 font-medium">
Metadata:
</div>
<div className="space-y-1">
{Object.entries(result.attributes).map(
([key, value]) => (
<div
key={key}
className="text-xs flex gap-2"
>
<span className="text-muted-foreground font-medium">
{key}:
</span>
<span className="text-foreground font-mono break-all">
{String(value)}
</span>
</div>
),
)}
</div>
</div>
</div>
)}
)}
</div>
</div>
)}
@ -117,4 +157,3 @@ export function SearchResultsDisplay({ searchResults }: SearchResultsDisplayProp
</div>
);
}

View file

@ -1,16 +1,25 @@
import React, { useState } from "react";
import { Card, Text } from "@tremor/react";
import { Select } from "antd";
import { PlusIcon, TrashIcon, GripVerticalIcon } from "lucide-react";
import { Card } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { GripVertical, Plus, Trash } from "lucide-react";
import { cn } from "@/lib/utils";
import VariableTextArea from "../variable_textarea";
import { Message } from "./types";
const { Option } = Select;
interface PromptMessagesCardProps {
messages: Message[];
onAddMessage: () => void;
onUpdateMessage: (index: number, field: "role" | "content", value: string) => void;
onUpdateMessage: (
index: number,
field: "role" | "content",
value: string,
) => void;
onRemoveMessage: (index: number) => void;
onMoveMessage: (fromIndex: number, toIndex: number) => void;
}
@ -51,10 +60,14 @@ const PromptMessagesCard: React.FC<PromptMessagesCardProps> = ({
return (
<Card className="p-3">
<div className="mb-2">
<Text className="text-sm font-medium">Prompt messages</Text>
<Text className="text-gray-500 text-xs mt-1">
Use <code className="bg-gray-100 px-1 rounded text-xs">{'{{variable}}'}</code> syntax for template variables
</Text>
<div className="text-sm font-medium">Prompt messages</div>
<div className="text-muted-foreground text-xs mt-1">
Use{" "}
<code className="bg-muted px-1 rounded text-xs">
{"{{variable}}"}
</code>{" "}
syntax for template variables
</div>
</div>
<div className="space-y-2">
{messages.map((message, index) => (
@ -65,33 +78,41 @@ const PromptMessagesCard: React.FC<PromptMessagesCardProps> = ({
onDragOver={(e) => handleDragOver(e, index)}
onDrop={(e) => handleDrop(e, index)}
onDragEnd={handleDragEnd}
className={`border border-gray-300 rounded overflow-hidden bg-white transition-all ${
draggedIndex === index ? "opacity-50" : ""
} ${dragOverIndex === index && draggedIndex !== index ? "border-blue-500 border-2" : ""}`}
className={cn(
"border border-border rounded overflow-hidden bg-background transition-all",
draggedIndex === index && "opacity-50",
dragOverIndex === index &&
draggedIndex !== index &&
"border-primary border-2",
)}
>
<div className="bg-gray-50 px-2 py-1.5 border-b border-gray-300 flex items-center justify-between">
<div className="bg-muted px-2 py-1.5 border-b border-border flex items-center justify-between">
<Select
value={message.role}
onChange={(value) => onUpdateMessage(index, "role", value)}
style={{ width: 100 }}
size="small"
bordered={false}
onValueChange={(v) => onUpdateMessage(index, "role", v)}
>
<Option value="user">User</Option>
<Option value="assistant">Assistant</Option>
<Option value="system">System</Option>
<SelectTrigger className="w-[100px] h-7 border-0 bg-transparent shadow-none">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">User</SelectItem>
<SelectItem value="assistant">Assistant</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
<div className="flex items-center gap-1">
{messages.length > 1 && (
<button
type="button"
onClick={() => onRemoveMessage(index)}
className="text-gray-400 hover:text-red-500"
className="text-muted-foreground hover:text-destructive"
aria-label="Remove message"
>
<TrashIcon size={14} />
<Trash size={14} />
</button>
)}
<div className="cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600">
<GripVerticalIcon size={16} />
<div className="cursor-grab active:cursor-grabbing text-muted-foreground hover:text-foreground">
<GripVertical size={16} />
</div>
</div>
</div>
@ -107,10 +128,11 @@ const PromptMessagesCard: React.FC<PromptMessagesCardProps> = ({
))}
</div>
<button
type="button"
onClick={onAddMessage}
className="mt-2 text-xs text-blue-600 hover:text-blue-700 flex items-center"
className="mt-2 text-xs text-primary hover:underline flex items-center"
>
<PlusIcon size={14} className="mr-1" />
<Plus size={14} className="mr-1" />
Add message
</button>
</Card>
@ -118,4 +140,3 @@ const PromptMessagesCard: React.FC<PromptMessagesCardProps> = ({
};
export default PromptMessagesCard;