diff --git a/src/components/ai/AiChat.tsx b/src/components/ai/AiChat.tsx
index a84166c07..5381e2439 100644
--- a/src/components/ai/AiChat.tsx
+++ b/src/components/ai/AiChat.tsx
@@ -5,7 +5,7 @@ import {
ConversationEmptyState,
ConversationScrollButton
} from "@flanksource-ui/components/ai-elements/conversation";
-import { SquarePen, X } from "lucide-react";
+import { Check, History, SquarePen, Trash2, X } from "lucide-react";
import { Loader } from "@flanksource-ui/components/ai-elements/loader";
import {
Confirmation,
@@ -44,6 +44,14 @@ import {
} from "@flanksource-ui/components/ai-elements/suggestion";
import { Button } from "@flanksource-ui/components/ui/button";
import { Card } from "@flanksource-ui/components/ui/card";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger
+} from "@flanksource-ui/components/ui/dropdown-menu";
import {
ChartConfig,
ChartContainer,
@@ -51,6 +59,10 @@ import {
ChartTooltipContent
} from "@flanksource-ui/components/ui/chart";
import { formatTick, parseTimestamp } from "@flanksource-ui/lib/timeseries";
+import {
+ saveAIConversation,
+ type AIConversationRecord
+} from "@flanksource-ui/lib/ai-chat-history";
import { cn } from "@flanksource-ui/lib/utils";
import type { FileUIPart, ReasoningUIPart, UIMessage } from "ai";
import { getToolName, isToolUIPart } from "ai";
@@ -66,6 +78,45 @@ type PlotTimeseriesOutput = {
title?: string;
};
+const HISTORY_PREVIEW_MAX_LENGTH = 72;
+
+function getConversationPreview(messages: UIMessage[]): string {
+ for (const message of messages) {
+ if (message.role !== "user") {
+ continue;
+ }
+
+ for (const part of message.parts) {
+ if (part.type !== "text") {
+ continue;
+ }
+
+ const normalizedText = part.text.replace(/\s+/g, " ").trim();
+
+ if (!normalizedText) {
+ continue;
+ }
+
+ if (normalizedText.length <= HISTORY_PREVIEW_MAX_LENGTH) {
+ return normalizedText;
+ }
+
+ return `${normalizedText.slice(0, HISTORY_PREVIEW_MAX_LENGTH - 1)}…`;
+ }
+ }
+
+ return "Untitled conversation";
+}
+
+function formatConversationTime(updatedAt: number): string {
+ return new Date(updatedAt).toLocaleString("en-US", {
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit"
+ });
+}
+
const isPlotTimeseriesOutput = (
output: unknown
): output is PlotTimeseriesOutput => {
@@ -190,6 +241,15 @@ export type AIChatProps = {
onClose?: () => void;
onNewChat?: () => void;
quickPrompts?: string[];
+ activeConversationId?: string;
+ conversationHistory?: AIConversationRecord[];
+ onSelectConversation?: (conversationId: string) => void;
+ onDeleteConversation?: (conversationId: string) => void;
+ onConversationPersisted?: (
+ conversationId: string,
+ messages: UIMessage[]
+ ) => void;
+ storageScopeKey?: string;
};
export function AIChat({
@@ -198,7 +258,13 @@ export function AIChat({
id,
onClose,
onNewChat,
- quickPrompts
+ quickPrompts,
+ activeConversationId,
+ conversationHistory,
+ onSelectConversation,
+ onDeleteConversation,
+ onConversationPersisted,
+ storageScopeKey
}: AIChatProps) {
const {
messages,
@@ -214,6 +280,15 @@ export function AIChat({
id
});
+ useEffect(() => {
+ if (!chat.id || messages.length === 0) {
+ return;
+ }
+
+ void saveAIConversation(chat.id, messages, storageScopeKey);
+ onConversationPersisted?.(chat.id, messages);
+ }, [chat.id, messages, onConversationPersisted, storageScopeKey]);
+
// Auto-send when chat mounts with a pre-seeded user message (e.g. from setChatMessages).
const hasSentOnMount = useRef(false);
useEffect(() => {
@@ -265,11 +340,15 @@ export function AIChat({
);
const handleNewChat = useCallback(() => {
- setMessages([]);
+ if (onNewChat) {
+ onNewChat();
+ } else {
+ setMessages([]);
+ }
+
if (error) {
clearError();
}
- onNewChat?.();
}, [clearError, error, onNewChat, setMessages]);
const handleSuggestionClick = useCallback(
@@ -482,6 +561,10 @@ export function AIChat({
);
};
+ const visibleConversationHistory = useMemo(() => {
+ return (conversationHistory ?? []).slice(0, 20);
+ }, [conversationHistory]);
+
const errorMessage = error
? error instanceof Error
? error.message
@@ -492,6 +575,75 @@ export function AIChat({
+ {onSelectConversation ? (
+
+
+
+
+
+ Conversation History
+
+ {visibleConversationHistory.length > 0 ? (
+ visibleConversationHistory.map((conversation) => {
+ const isActive = activeConversationId === conversation.id;
+
+ return (
+ onSelectConversation(conversation.id)}
+ >
+
+
+ {getConversationPreview(conversation.messages)}
+
+
+ {formatConversationTime(conversation.updatedAt)}
+
+
+
+ {isActive ? (
+
+ ) : null}
+ {onDeleteConversation ? (
+
+ ) : null}
+
+
+ );
+ })
+ ) : (
+
+ No saved conversations yet
+
+ )}
+
+
+ ) : null}
+
{onNewChat ? (