mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-30 19:09:27 +02:00
Compare commits
1 Commits
agent/lamb
...
agent/emac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4107ab84d0 |
@@ -12,6 +12,7 @@ export function useCreateChatSession() {
|
||||
api.createChatSession(data),
|
||||
onSettled: () => {
|
||||
qc.invalidateQueries({ queryKey: chatKeys.sessions(wsId) });
|
||||
qc.invalidateQueries({ queryKey: chatKeys.allSessions(wsId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -24,6 +25,7 @@ export function useArchiveChatSession() {
|
||||
mutationFn: (sessionId: string) => api.archiveChatSession(sessionId),
|
||||
onSettled: () => {
|
||||
qc.invalidateQueries({ queryKey: chatKeys.sessions(wsId) });
|
||||
qc.invalidateQueries({ queryKey: chatKeys.allSessions(wsId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { api } from "@/platform/api";
|
||||
export const chatKeys = {
|
||||
all: (wsId: string) => ["chat", wsId] as const,
|
||||
sessions: (wsId: string) => [...chatKeys.all(wsId), "sessions"] as const,
|
||||
allSessions: (wsId: string) => [...chatKeys.all(wsId), "sessions", "all"] as const,
|
||||
session: (wsId: string, id: string) => [...chatKeys.all(wsId), "session", id] as const,
|
||||
messages: (sessionId: string) => ["chat", "messages", sessionId] as const,
|
||||
};
|
||||
@@ -16,6 +17,14 @@ export function chatSessionsOptions(wsId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function allChatSessionsOptions(wsId: string) {
|
||||
return queryOptions({
|
||||
queryKey: chatKeys.allSessions(wsId),
|
||||
queryFn: () => api.listChatSessions({ status: "all" }),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
export function chatSessionOptions(wsId: string, id: string) {
|
||||
return queryOptions({
|
||||
queryKey: chatKeys.session(wsId, id),
|
||||
|
||||
@@ -7,22 +7,23 @@ interface ChatInputProps {
|
||||
onSend: (content: string) => void;
|
||||
onStop?: () => void;
|
||||
isRunning?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function ChatInput({ onSend, onStop, isRunning }: ChatInputProps) {
|
||||
export function ChatInput({ onSend, onStop, isRunning, disabled }: ChatInputProps) {
|
||||
const [value, setValue] = useState("");
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const handleSend = useCallback(() => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || isRunning) return;
|
||||
if (!trimmed || isRunning || disabled) return;
|
||||
onSend(trimmed);
|
||||
setValue("");
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = "auto";
|
||||
}
|
||||
textareaRef.current?.focus();
|
||||
}, [value, isRunning, onSend]);
|
||||
}, [value, isRunning, disabled, onSend]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
@@ -52,8 +53,8 @@ export function ChatInput({ onSend, onStop, isRunning }: ChatInputProps) {
|
||||
handleInput();
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask Multica..."
|
||||
disabled={isRunning}
|
||||
placeholder={disabled ? "This session is archived" : "Ask Multica..."}
|
||||
disabled={isRunning || disabled}
|
||||
className="block w-full resize-none bg-transparent px-3 pt-3 pb-2 text-sm placeholder:text-muted-foreground focus:outline-none disabled:opacity-50"
|
||||
rows={1}
|
||||
/>
|
||||
@@ -68,7 +69,7 @@ export function ChatInput({ onSend, onStop, isRunning }: ChatInputProps) {
|
||||
) : (
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!value.trim()}
|
||||
disabled={!value.trim() || disabled}
|
||||
className="flex size-7 items-center justify-center rounded-full bg-foreground text-background transition-opacity hover:opacity-80 disabled:opacity-30"
|
||||
>
|
||||
<ArrowUp className="size-4" />
|
||||
|
||||
202
apps/web/features/chat/components/chat-session-history.tsx
Normal file
202
apps/web/features/chat/components/chat-session-history.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ArrowLeft, MessageSquare, Archive, Trash2 } from "lucide-react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@multica/ui/components/ui/avatar";
|
||||
import { Bot } from "lucide-react";
|
||||
import { useWorkspaceId } from "@multica/core/hooks";
|
||||
import { agentListOptions } from "@multica/core/workspace/queries";
|
||||
import { allChatSessionsOptions } from "@/core/chat/queries";
|
||||
import { useArchiveChatSession } from "@/core/chat/mutations";
|
||||
import { useChatStore } from "../store";
|
||||
import type { ChatSession, Agent } from "@multica/core/types";
|
||||
|
||||
export function ChatSessionHistory() {
|
||||
const wsId = useWorkspaceId();
|
||||
const setShowHistory = useChatStore((s) => s.setShowHistory);
|
||||
const setActiveSession = useChatStore((s) => s.setActiveSession);
|
||||
const clearTimeline = useChatStore((s) => s.clearTimeline);
|
||||
const setPendingTask = useChatStore((s) => s.setPendingTask);
|
||||
const activeSessionId = useChatStore((s) => s.activeSessionId);
|
||||
|
||||
const { data: sessions = [] } = useQuery(allChatSessionsOptions(wsId));
|
||||
const { data: agents = [] } = useQuery(agentListOptions(wsId));
|
||||
const archiveSession = useArchiveChatSession();
|
||||
|
||||
const agentMap = new Map(agents.map((a) => [a.id, a]));
|
||||
|
||||
const handleSelectSession = (session: ChatSession) => {
|
||||
setActiveSession(session.id);
|
||||
clearTimeline();
|
||||
setPendingTask(null);
|
||||
setShowHistory(false);
|
||||
};
|
||||
|
||||
const handleArchive = (e: React.MouseEvent, sessionId: string) => {
|
||||
e.stopPropagation();
|
||||
archiveSession.mutate(sessionId);
|
||||
if (activeSessionId === sessionId) {
|
||||
setActiveSession(null);
|
||||
}
|
||||
};
|
||||
|
||||
const activeSessions = sessions.filter((s) => s.status === "active");
|
||||
const archivedSessions = sessions.filter((s) => s.status === "archived");
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 border-b px-4 py-2.5">
|
||||
<button
|
||||
onClick={() => setShowHistory(false)}
|
||||
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<ArrowLeft className="size-3.5" />
|
||||
</button>
|
||||
<span className="text-sm font-medium">Chat History</span>
|
||||
</div>
|
||||
|
||||
{/* Session list */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{sessions.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-12 text-muted-foreground">
|
||||
<MessageSquare className="size-6" />
|
||||
<span className="text-sm">No chat sessions yet</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{activeSessions.length > 0 && (
|
||||
<SessionGroup
|
||||
label="Active"
|
||||
sessions={activeSessions}
|
||||
agentMap={agentMap}
|
||||
activeSessionId={activeSessionId}
|
||||
onSelect={handleSelectSession}
|
||||
onArchive={handleArchive}
|
||||
/>
|
||||
)}
|
||||
{archivedSessions.length > 0 && (
|
||||
<SessionGroup
|
||||
label="Archived"
|
||||
sessions={archivedSessions}
|
||||
agentMap={agentMap}
|
||||
activeSessionId={activeSessionId}
|
||||
onSelect={handleSelectSession}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionGroup({
|
||||
label,
|
||||
sessions,
|
||||
agentMap,
|
||||
activeSessionId,
|
||||
onSelect,
|
||||
onArchive,
|
||||
}: {
|
||||
label: string;
|
||||
sessions: ChatSession[];
|
||||
agentMap: Map<string, Agent>;
|
||||
activeSessionId: string | null;
|
||||
onSelect: (session: ChatSession) => void;
|
||||
onArchive?: (e: React.MouseEvent, sessionId: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="px-4 pt-3 pb-1">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{sessions.map((session) => (
|
||||
<SessionItem
|
||||
key={session.id}
|
||||
session={session}
|
||||
agent={agentMap.get(session.agent_id) ?? null}
|
||||
isActive={session.id === activeSessionId}
|
||||
onSelect={() => onSelect(session)}
|
||||
onArchive={onArchive ? (e) => onArchive(e, session.id) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionItem({
|
||||
session,
|
||||
agent,
|
||||
isActive,
|
||||
onSelect,
|
||||
onArchive,
|
||||
}: {
|
||||
session: ChatSession;
|
||||
agent: Agent | null;
|
||||
isActive: boolean;
|
||||
onSelect: () => void;
|
||||
onArchive?: (e: React.MouseEvent) => void;
|
||||
}) {
|
||||
const timeAgo = formatTimeAgo(session.updated_at);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onSelect}
|
||||
className={`group flex w-full items-start gap-3 px-4 py-2.5 text-left transition-colors hover:bg-accent/50 ${
|
||||
isActive ? "bg-accent/30" : ""
|
||||
}`}
|
||||
>
|
||||
<Avatar className="size-6 shrink-0 mt-0.5">
|
||||
{agent?.avatar_url && <AvatarImage src={agent.avatar_url} />}
|
||||
<AvatarFallback className="bg-purple-100 text-purple-700 text-[10px]">
|
||||
<Bot className="size-3" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-sm font-medium">
|
||||
{session.title || "Untitled"}
|
||||
</span>
|
||||
{session.status === "archived" && (
|
||||
<Archive className="size-3 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
{agent && (
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{agent.name}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground/60">{timeAgo}</span>
|
||||
</div>
|
||||
</div>
|
||||
{onArchive && (
|
||||
<button
|
||||
onClick={onArchive}
|
||||
title="Archive"
|
||||
className="invisible group-hover:visible flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-destructive shrink-0 mt-0.5"
|
||||
>
|
||||
<Trash2 className="size-3" />
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function formatTimeAgo(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return "just now";
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Minus, Maximize2, Minimize2, Send, ChevronDown, Bot, Plus } from "lucide-react";
|
||||
import { Minus, Maximize2, Minimize2, Send, ChevronDown, Bot, Plus, History } from "lucide-react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@multica/ui/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -15,11 +15,12 @@ import { useAuthStore } from "@multica/core/auth";
|
||||
import { agentListOptions, memberListOptions } from "@multica/core/workspace/queries";
|
||||
import { canAssignAgent } from "@multica/views/issues/components";
|
||||
import { api } from "@/platform/api";
|
||||
import { chatSessionsOptions, chatMessagesOptions, chatKeys } from "@/core/chat/queries";
|
||||
import { chatSessionsOptions, allChatSessionsOptions, chatMessagesOptions, chatKeys } from "@/core/chat/queries";
|
||||
import { useCreateChatSession } from "@/core/chat/mutations";
|
||||
import { useChatStore } from "../store";
|
||||
import { ChatMessageList } from "./chat-message-list";
|
||||
import { ChatInput } from "./chat-input";
|
||||
import { ChatSessionHistory } from "./chat-session-history";
|
||||
import { useWS } from "@multica/core/realtime";
|
||||
import type { TaskMessagePayload, ChatDonePayload, Agent, ChatMessage } from "@multica/core/types";
|
||||
|
||||
@@ -33,22 +34,31 @@ export function ChatWindow() {
|
||||
const selectedAgentId = useChatStore((s) => s.selectedAgentId);
|
||||
const setOpen = useChatStore((s) => s.setOpen);
|
||||
const toggleFullscreen = useChatStore((s) => s.toggleFullscreen);
|
||||
const showHistory = useChatStore((s) => s.showHistory);
|
||||
const setActiveSession = useChatStore((s) => s.setActiveSession);
|
||||
const setPendingTask = useChatStore((s) => s.setPendingTask);
|
||||
const addTimelineItem = useChatStore((s) => s.addTimelineItem);
|
||||
const clearTimeline = useChatStore((s) => s.clearTimeline);
|
||||
const setSelectedAgentId = useChatStore((s) => s.setSelectedAgentId);
|
||||
const setShowHistory = useChatStore((s) => s.setShowHistory);
|
||||
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const { data: agents = [] } = useQuery(agentListOptions(wsId));
|
||||
const { data: members = [] } = useQuery(memberListOptions(wsId));
|
||||
const { data: sessions = [] } = useQuery(chatSessionsOptions(wsId));
|
||||
const { data: allSessions = [] } = useQuery(allChatSessionsOptions(wsId));
|
||||
const { data: rawMessages } = useQuery(
|
||||
chatMessagesOptions(activeSessionId ?? ""),
|
||||
);
|
||||
// When no active session, always show empty — don't use stale cache
|
||||
const messages = activeSessionId ? rawMessages ?? [] : [];
|
||||
|
||||
// Check if current session is archived
|
||||
const currentSession = activeSessionId
|
||||
? allSessions.find((s) => s.id === activeSessionId)
|
||||
: null;
|
||||
const isSessionArchived = currentSession?.status === "archived";
|
||||
|
||||
const qc = useQueryClient();
|
||||
const createSession = useCreateChatSession();
|
||||
|
||||
@@ -217,55 +227,75 @@ export function ChatWindow() {
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b px-4 py-2.5">
|
||||
<AgentSelector
|
||||
agents={availableAgents}
|
||||
activeAgent={activeAgent}
|
||||
onSelect={handleSelectAgent}
|
||||
/>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveSession(null);
|
||||
clearTimeline();
|
||||
setPendingTask(null);
|
||||
}}
|
||||
title="New chat"
|
||||
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
title={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
|
||||
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
{isFullscreen ? <Minimize2 className="size-3.5" /> : <Maximize2 className="size-3.5" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
title="Minimize"
|
||||
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<Minus className="size-3.5" />
|
||||
</button>
|
||||
{!showHistory && (
|
||||
<div className="flex items-center justify-between border-b px-4 py-2.5">
|
||||
<AgentSelector
|
||||
agents={availableAgents}
|
||||
activeAgent={activeAgent}
|
||||
onSelect={handleSelectAgent}
|
||||
/>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button
|
||||
onClick={() => setShowHistory(true)}
|
||||
title="Chat history"
|
||||
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<History className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveSession(null);
|
||||
clearTimeline();
|
||||
setPendingTask(null);
|
||||
}}
|
||||
title="New chat"
|
||||
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
title={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
|
||||
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
{isFullscreen ? <Minimize2 className="size-3.5" /> : <Maximize2 className="size-3.5" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
title="Minimize"
|
||||
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<Minus className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages or Empty State */}
|
||||
{hasMessages ? (
|
||||
<ChatMessageList
|
||||
messages={messages}
|
||||
agent={activeAgent}
|
||||
timelineItems={timelineItems}
|
||||
isWaiting={!!pendingTaskId}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState agentName={activeAgent?.name} />
|
||||
)}
|
||||
|
||||
{/* Input */}
|
||||
<ChatInput onSend={handleSend} onStop={handleStop} isRunning={!!pendingTaskId} />
|
||||
{showHistory ? (
|
||||
<ChatSessionHistory />
|
||||
) : (
|
||||
<>
|
||||
{/* Messages or Empty State */}
|
||||
{hasMessages ? (
|
||||
<ChatMessageList
|
||||
messages={messages}
|
||||
agent={activeAgent}
|
||||
timelineItems={timelineItems}
|
||||
isWaiting={!!pendingTaskId}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState agentName={activeAgent?.name} />
|
||||
)}
|
||||
|
||||
{/* Input — disabled for archived sessions */}
|
||||
<ChatInput
|
||||
onSend={handleSend}
|
||||
onStop={handleStop}
|
||||
isRunning={!!pendingTaskId}
|
||||
disabled={isSessionArchived}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ interface ChatState {
|
||||
activeSessionId: string | null;
|
||||
pendingTaskId: string | null;
|
||||
selectedAgentId: string | null;
|
||||
showHistory: boolean;
|
||||
timelineItems: ChatTimelineItem[];
|
||||
setOpen: (open: boolean) => void;
|
||||
toggle: () => void;
|
||||
@@ -30,6 +31,7 @@ interface ChatState {
|
||||
setActiveSession: (id: string | null) => void;
|
||||
setPendingTask: (taskId: string | null) => void;
|
||||
setSelectedAgentId: (id: string) => void;
|
||||
setShowHistory: (show: boolean) => void;
|
||||
addTimelineItem: (item: ChatTimelineItem) => void;
|
||||
clearTimeline: () => void;
|
||||
}
|
||||
@@ -40,6 +42,7 @@ export const useChatStore = create<ChatState>((set) => ({
|
||||
activeSessionId: readStored(SESSION_STORAGE_KEY),
|
||||
pendingTaskId: null,
|
||||
selectedAgentId: readStored(AGENT_STORAGE_KEY),
|
||||
showHistory: false,
|
||||
timelineItems: [],
|
||||
setOpen: (open) => set({ isOpen: open, ...(open ? {} : { isFullscreen: false }) }),
|
||||
toggle: () => set((s) => ({ isOpen: !s.isOpen, ...(s.isOpen ? { isFullscreen: false } : {}) })),
|
||||
@@ -57,6 +60,7 @@ export const useChatStore = create<ChatState>((set) => ({
|
||||
localStorage.setItem(AGENT_STORAGE_KEY, id);
|
||||
set({ selectedAgentId: id });
|
||||
},
|
||||
setShowHistory: (show) => set({ showHistory: show }),
|
||||
addTimelineItem: (item) =>
|
||||
set((s) => {
|
||||
if (s.timelineItems.some((t) => t.seq === item.seq)) return s;
|
||||
|
||||
@@ -608,8 +608,9 @@ export class ApiClient {
|
||||
}
|
||||
|
||||
// Chat Sessions
|
||||
async listChatSessions(): Promise<ChatSession[]> {
|
||||
return this.fetch("/api/chat/sessions");
|
||||
async listChatSessions(params?: { status?: string }): Promise<ChatSession[]> {
|
||||
const query = params?.status ? `?status=${params.status}` : "";
|
||||
return this.fetch(`/api/chat/sessions${query}`);
|
||||
}
|
||||
|
||||
async getChatSession(id: string): Promise<ChatSession> {
|
||||
|
||||
@@ -71,10 +71,21 @@ func (h *Handler) ListChatSessions(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
workspaceID := ctxWorkspaceID(r.Context())
|
||||
|
||||
sessions, err := h.Queries.ListChatSessionsByCreator(r.Context(), db.ListChatSessionsByCreatorParams{
|
||||
WorkspaceID: parseUUID(workspaceID),
|
||||
CreatorID: parseUUID(userID),
|
||||
})
|
||||
status := r.URL.Query().Get("status")
|
||||
|
||||
var sessions []db.ChatSession
|
||||
var err error
|
||||
if status == "all" {
|
||||
sessions, err = h.Queries.ListAllChatSessionsByCreator(r.Context(), db.ListAllChatSessionsByCreatorParams{
|
||||
WorkspaceID: parseUUID(workspaceID),
|
||||
CreatorID: parseUUID(userID),
|
||||
})
|
||||
} else {
|
||||
sessions, err = h.Queries.ListChatSessionsByCreator(r.Context(), db.ListChatSessionsByCreatorParams{
|
||||
WorkspaceID: parseUUID(workspaceID),
|
||||
CreatorID: parseUUID(userID),
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to list chat sessions")
|
||||
return
|
||||
|
||||
@@ -221,6 +221,48 @@ func (q *Queries) GetLastChatTaskSession(ctx context.Context, chatSessionID pgty
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listAllChatSessionsByCreator = `-- name: ListAllChatSessionsByCreator :many
|
||||
SELECT id, workspace_id, agent_id, creator_id, title, session_id, work_dir, status, created_at, updated_at FROM chat_session
|
||||
WHERE workspace_id = $1 AND creator_id = $2
|
||||
ORDER BY updated_at DESC
|
||||
`
|
||||
|
||||
type ListAllChatSessionsByCreatorParams struct {
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
CreatorID pgtype.UUID `json:"creator_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListAllChatSessionsByCreator(ctx context.Context, arg ListAllChatSessionsByCreatorParams) ([]ChatSession, error) {
|
||||
rows, err := q.db.Query(ctx, listAllChatSessionsByCreator, arg.WorkspaceID, arg.CreatorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []ChatSession{}
|
||||
for rows.Next() {
|
||||
var i ChatSession
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.WorkspaceID,
|
||||
&i.AgentID,
|
||||
&i.CreatorID,
|
||||
&i.Title,
|
||||
&i.SessionID,
|
||||
&i.WorkDir,
|
||||
&i.Status,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listChatMessages = `-- name: ListChatMessages :many
|
||||
SELECT id, chat_session_id, role, content, task_id, created_at FROM chat_message
|
||||
WHERE chat_session_id = $1
|
||||
|
||||
@@ -16,6 +16,11 @@ SELECT * FROM chat_session
|
||||
WHERE workspace_id = $1 AND creator_id = $2 AND status = 'active'
|
||||
ORDER BY updated_at DESC;
|
||||
|
||||
-- name: ListAllChatSessionsByCreator :many
|
||||
SELECT * FROM chat_session
|
||||
WHERE workspace_id = $1 AND creator_id = $2
|
||||
ORDER BY updated_at DESC;
|
||||
|
||||
-- name: UpdateChatSessionTitle :one
|
||||
UPDATE chat_session SET title = $2, updated_at = now()
|
||||
WHERE id = $1
|
||||
|
||||
Reference in New Issue
Block a user