diff --git a/src/components/ai/AssistChat.vue b/src/components/ai/AssistChat.vue index 54e93ca..ccfa623 100644 --- a/src/components/ai/AssistChat.vue +++ b/src/components/ai/AssistChat.vue @@ -42,16 +42,18 @@ Past conversations -
+
+

Loading conversations

+
+

Your past conversations will appear here.

- -

History is a preview; saved conversations arrive with persistence.

@@ -265,6 +267,7 @@ const newChat = () => { const toggleHistory = () => { view.value = view.value === "history" ? "chat" : "history"; + if (view.value === "history") store.listSessions(); }; const sampleQueries = computed(() => @@ -286,22 +289,21 @@ const runSample = (q: string) => { store.send(q); }; -interface PastSession { - id: string; - title: string; - when: string; -} - -// Placeholder list so the history flow is visible; real data arrives with the -// chat-persistence API. -const pastSessions = ref([ - { id: "1", title: "Why is wordpress-blog unhealthy?", when: "2 hours ago" }, - { id: "2", title: "Summarize recent security events", when: "Yesterday" }, - { id: "3", title: "Which containers restarted recently?", when: "3 days ago" }, -]); +const formatWhen = (iso: string): string => { + const then = new Date(iso).getTime(); + if (Number.isNaN(then)) return ""; + const mins = Math.floor((Date.now() - then) / 60000); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins}m ago`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 7) return `${days}d ago`; + return new Date(then).toLocaleDateString(); +}; -const loadPast = (_session: PastSession) => { - // Loading a stored conversation needs the persistence API; return to chat. +const loadPast = async (id: string) => { + await store.loadSession(id); view.value = "chat"; }; diff --git a/src/services/api.ts b/src/services/api.ts index be4204a..a392680 100755 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -457,8 +457,19 @@ export interface AISession { suggested_actions: AISuggestedAction[]; } +export interface AISessionSummary { + id: string; + scope: "system" | "deployment"; + deployment?: string; + status: "ready" | "awaiting_approval"; + title: string; + created_at: string; + updated_at: string; +} + export const aiApi = { status: () => apiClient.get("/ai/status"), + listSessions: () => apiClient.get<{ sessions: AISessionSummary[] }>("/ai/sessions"), createSession: (body: { scope: "system" | "deployment"; deployment?: string; diff --git a/src/stores/assist.test.ts b/src/stores/assist.test.ts index d580978..49efde9 100644 --- a/src/stores/assist.test.ts +++ b/src/stores/assist.test.ts @@ -10,6 +10,8 @@ vi.mock("@/services/api", () => ({ createSession: vi.fn(), sessionMessage: vi.fn(), approveSession: vi.fn(), + listSessions: vi.fn(), + getSession: vi.fn(), }, containersApi: { exec: vi.fn() }, deploymentsApi: { serviceAction: vi.fn(), getServices: vi.fn() }, @@ -55,6 +57,38 @@ describe("assist store", () => { expect(store.session).toBeNull(); }); + it("lists saved sessions for the history view", async () => { + const { aiApi } = await import("@/services/api"); + vi.mocked(aiApi.listSessions).mockResolvedValue({ + data: { + sessions: [ + { id: "ais_2", scope: "system", status: "ready", title: "recent one", updated_at: "2026-07-18T10:00:00Z" }, + ], + }, + } as any); + + const store = useAssistStore(); + await store.listSessions(); + + expect(store.sessions).toHaveLength(1); + expect(store.sessions[0].title).toBe("recent one"); + }); + + it("resumes a saved session and binds later turns to its scope", async () => { + const { aiApi } = await import("@/services/api"); + vi.mocked(aiApi.getSession).mockResolvedValue({ + data: session({ id: "ais_9", scope: "deployment", deployment: "shop" }), + } as any); + + const store = useAssistStore(); + await store.loadSession("ais_9"); + + expect(aiApi.getSession).toHaveBeenCalledWith("ais_9"); + expect(store.session?.id).toBe("ais_9"); + expect(store.scope).toBe("deployment"); + expect(store.deployment).toBe("shop"); + }); + it("creates a session from a seed message", async () => { await enable(); const { aiApi } = await import("@/services/api"); diff --git a/src/stores/assist.ts b/src/stores/assist.ts index e20c18c..6aead75 100644 --- a/src/stores/assist.ts +++ b/src/stores/assist.ts @@ -1,6 +1,13 @@ import { defineStore } from "pinia"; import { ref } from "vue"; -import { aiApi, containersApi, deploymentsApi, type AISession, type AISuggestedAction } from "@/services/api"; +import { + aiApi, + containersApi, + deploymentsApi, + type AISession, + type AISessionSummary, + type AISuggestedAction, +} from "@/services/api"; import { useAIStore } from "@/stores/ai"; import { usePlanFlowStore } from "@/stores/planFlow"; @@ -41,6 +48,9 @@ export const useAssistStore = defineStore("assist", () => { // Per-call decisions while an approval batch is open; once every // pending call is decided the batch is submitted. const decisions = ref>({}); + // Saved sessions the current user can resume. + const sessions = ref([]); + const listing = ref(false); function reset() { session.value = null; @@ -173,6 +183,37 @@ export const useAssistStore = defineStore("assist", () => { } } + // listSessions loads the user's saved conversations for the history view. + async function listSessions() { + listing.value = true; + try { + const { data } = await aiApi.listSessions(); + sessions.value = data.sessions || []; + } catch (err: any) { + error.value = err.response?.data?.error || err.message; + } finally { + listing.value = false; + } + } + + // loadSession reopens a saved conversation and binds later turns to its scope. + async function loadSession(id: string) { + loading.value = true; + error.value = ""; + suggestionOutputs.value = {}; + decisions.value = {}; + try { + const { data } = await aiApi.getSession(id); + session.value = data; + scope.value = data.scope; + deployment.value = data.deployment; + } catch (err: any) { + error.value = err.response?.data?.error || err.message; + } finally { + loading.value = false; + } + } + function close() { visible.value = false; embedded.value = false; @@ -192,8 +233,12 @@ export const useAssistStore = defineStore("assist", () => { runningIndex, suggestionOutputs, decisions, + sessions, + listing, open, send, + listSessions, + loadSession, decide, approveAll, declineAll,