Merge branch 'hackathon/copilot' of github.com:Significant-Gravitas/AutoGPT into hackathon/copilot

This commit is contained in:
Swifty
2025-12-16 17:26:25 +01:00
7 changed files with 307 additions and 25 deletions

View File

@@ -5188,11 +5188,67 @@
}
},
"/api/chat/sessions": {
"get": {
"tags": ["v2", "chat", "chat"],
"summary": "List Sessions",
"description": "List chat sessions for the authenticated user.\n\nReturns a paginated list of chat sessions belonging to the current user,\nordered by most recently updated.\n\nArgs:\n user_id: The authenticated user's ID.\n limit: Maximum number of sessions to return (1-100).\n offset: Number of sessions to skip for pagination.\n\nReturns:\n ListSessionsResponse: List of session summaries and total count.",
"operationId": "getV2ListSessions",
"security": [{ "HTTPBearerJWT": [] }],
"parameters": [
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"maximum": 100,
"minimum": 1,
"default": 50,
"title": "Limit"
}
},
{
"name": "offset",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"minimum": 0,
"default": 0,
"title": "Offset"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ListSessionsResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
},
"401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
}
}
},
"post": {
"tags": ["v2", "chat", "chat"],
"summary": "Create Session",
"description": "Create a new chat session.\n\nInitiates a new chat session for either an authenticated or anonymous user.\n\nArgs:\n user_id: The optional authenticated user ID parsed from the JWT. If missing, creates an anonymous session.\n\nReturns:\n CreateSessionResponse: Details of the created session.",
"operationId": "postV2CreateSession",
"security": [{ "HTTPBearerJWT": [] }],
"responses": {
"200": {
"description": "Successful Response",
@@ -5207,8 +5263,7 @@
"401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
}
},
"security": [{ "HTTPBearerJWT": [] }]
}
}
},
"/api/chat/sessions/{session_id}": {
@@ -7505,6 +7560,20 @@
"required": ["source_id", "sink_id", "source_name", "sink_name"],
"title": "Link"
},
"ListSessionsResponse": {
"properties": {
"sessions": {
"items": { "$ref": "#/components/schemas/SessionSummaryResponse" },
"type": "array",
"title": "Sessions"
},
"total": { "type": "integer", "title": "Total" }
},
"type": "object",
"required": ["sessions", "total"],
"title": "ListSessionsResponse",
"description": "Response model for listing chat sessions."
},
"LogRawMetricRequest": {
"properties": {
"metric_name": {
@@ -8755,6 +8824,21 @@
"title": "SessionDetailResponse",
"description": "Response model providing complete details for a chat session, including messages."
},
"SessionSummaryResponse": {
"properties": {
"id": { "type": "string", "title": "Id" },
"created_at": { "type": "string", "title": "Created At" },
"updated_at": { "type": "string", "title": "Updated At" },
"title": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Title"
}
},
"type": "object",
"required": ["id", "created_at", "updated_at"],
"title": "SessionSummaryResponse",
"description": "Response model for a session summary (without messages)."
},
"SetGraphActiveVersion": {
"properties": {
"active_graph_version": {

View File

@@ -3,10 +3,12 @@
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
import React from "react";
import { List } from "@phosphor-icons/react";
import React, { useState } from "react";
import { ChatContainer } from "./components/ChatContainer/ChatContainer";
import { ChatErrorState } from "./components/ChatErrorState/ChatErrorState";
import { ChatLoadingState } from "./components/ChatLoadingState/ChatLoadingState";
import { SessionsDrawer } from "./components/SessionsDrawer/SessionsDrawer";
import { useChat } from "./useChat";
export interface ChatProps {
@@ -37,40 +39,49 @@ export function Chat({
createSession,
clearSession,
refreshSession,
loadSession,
} = useChat();
const [isSessionsDrawerOpen, setIsSessionsDrawerOpen] = useState(false);
const handleNewChat = () => {
clearSession();
onNewChat?.();
};
const handleSelectSession = async (sessionId: string) => {
try {
await loadSession(sessionId);
} catch (err) {
console.error("Failed to load session:", err);
}
};
return (
<div className={cn("flex h-full flex-col", className)}>
{/* Header */}
{showHeader && (
<header className="shrink-0 border-b border-zinc-200 bg-white p-3">
<div className="flex items-center justify-between">
{typeof headerTitle === "string" ? (
<Text variant="h2" className="text-lg font-semibold">
{headerTitle}
</Text>
) : (
headerTitle
)}
<div className="flex items-center gap-3">
<button
aria-label="View sessions"
onClick={() => setIsSessionsDrawerOpen(true)}
className="flex size-8 items-center justify-center rounded hover:bg-zinc-100"
>
<List width="1.25rem" height="1.25rem" />
</button>
{typeof headerTitle === "string" ? (
<Text variant="h2" className="text-lg font-semibold">
{headerTitle}
</Text>
) : (
headerTitle
)}
</div>
<div className="flex items-center gap-3">
{showSessionInfo && sessionId && (
<>
<Text variant="body">
<span
className="inline-block bg-gradient-to-r from-indigo-500 via-purple-500 to-indigo-500 bg-clip-text text-transparent"
style={{
backgroundSize: "200% 100%",
animation: "shimmer 2s ease-in-out infinite",
}}
>
Session: {sessionId.slice(0, 8)}...
</span>
</Text>
{showNewChatButton && (
<Button
variant="outline"
@@ -112,6 +123,14 @@ export function Chat({
/>
)}
</main>
{/* Sessions Drawer */}
<SessionsDrawer
isOpen={isSessionsDrawerOpen}
onClose={() => setIsSessionsDrawerOpen(false)}
onSelectSession={handleSelectSession}
currentSessionId={sessionId}
/>
</div>
);
}

View File

@@ -1,6 +1,25 @@
import type { ToolResult } from "@/types/chat";
import type { ChatMessageData } from "../ChatMessage/useChatMessage";
export function removePageContext(content: string): string {
// Remove "Page URL: ..." pattern (case insensitive, handles various formats)
let cleaned = content.replace(/Page URL:\s*[^\n\r]*/gi, "");
// Find "User Message:" marker to preserve the actual user message
const userMessageMatch = cleaned.match(/User Message:\s*([\s\S]*)$/i);
if (userMessageMatch) {
// If we found "User Message:", extract everything after it
cleaned = userMessageMatch[1];
} else {
// If no "User Message:" marker, remove "Page Content:" and everything after it
cleaned = cleaned.replace(/Page Content:[\s\S]*$/gi, "");
}
// Clean up extra whitespace and newlines
cleaned = cleaned.replace(/\n\s*\n\s*\n+/g, "\n\n").trim();
return cleaned;
}
export function createUserMessage(content: string): ChatMessageData {
return {
type: "message",

View File

@@ -10,6 +10,7 @@ import {
isToolCallArray,
isValidMessage,
parseToolResponse,
removePageContext,
} from "./helpers";
interface UseChatContainerArgs {
@@ -45,9 +46,19 @@ export function useChatContainer({
);
})
.map((msg: Record<string, unknown>) => {
const content = String(msg.content || "");
let content = String(msg.content || "");
const role = String(msg.role || "assistant").toLowerCase();
const toolCalls = msg.tool_calls;
// Remove page context from user messages when loading existing sessions
if (role === "user") {
content = removePageContext(content);
// Skip user messages that become empty after removing page context
if (!content.trim()) {
return null;
}
}
if (
role === "assistant" &&
toolCalls &&
@@ -71,6 +82,10 @@ export function useChatContainer({
}
return toolResponse;
}
// Skip assistant messages with empty content (they're handled by tool calls)
if (role === "assistant" && !content.trim()) {
return null;
}
return {
type: "message",
role: role as "user" | "assistant" | "system",

View File

@@ -0,0 +1,136 @@
"use client";
import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat";
import { Text } from "@/components/atoms/Text/Text";
import { scrollbarStyles } from "@/components/styles/scrollbars";
import { cn } from "@/lib/utils";
import { X } from "@phosphor-icons/react";
import { formatDistanceToNow } from "date-fns";
import { Drawer } from "vaul";
interface SessionsDrawerProps {
isOpen: boolean;
onClose: () => void;
onSelectSession: (sessionId: string) => void;
currentSessionId?: string | null;
}
export function SessionsDrawer({
isOpen,
onClose,
onSelectSession,
currentSessionId,
}: SessionsDrawerProps) {
const { data, isLoading } = useGetV2ListSessions(
{ limit: 100 },
{
query: {
enabled: isOpen,
},
},
);
const sessions =
data?.status === 200
? data.data.sessions.filter((session) => {
// Filter out sessions without messages (sessions that were never updated)
// If updated_at equals created_at, the session was created but never had messages
return session.updated_at !== session.created_at;
})
: [];
function handleSelectSession(sessionId: string) {
onSelectSession(sessionId);
onClose();
}
return (
<Drawer.Root
open={isOpen}
onOpenChange={(open) => !open && onClose()}
direction="right"
>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 z-[60] bg-black/10 backdrop-blur-sm" />
<Drawer.Content
className={cn(
"fixed right-0 top-0 z-[70] flex h-full w-96 flex-col border-l border-zinc-200 bg-white",
scrollbarStyles,
)}
>
<div className="shrink-0 p-4">
<div className="flex items-center justify-between">
<Drawer.Title className="text-lg font-semibold">
Chat Sessions
</Drawer.Title>
<button
aria-label="Close"
onClick={onClose}
className="flex size-8 items-center justify-center rounded hover:bg-zinc-100"
>
<X width="1.25rem" height="1.25rem" />
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Text variant="body" className="text-zinc-500">
Loading sessions...
</Text>
</div>
) : sessions.length === 0 ? (
<div className="flex items-center justify-center py-8">
<Text variant="body" className="text-zinc-500">
No sessions found
</Text>
</div>
) : (
<div className="space-y-2">
{sessions.map((session) => {
const isActive = session.id === currentSessionId;
const updatedAt = session.updated_at
? formatDistanceToNow(new Date(session.updated_at), {
addSuffix: true,
})
: "";
return (
<button
key={session.id}
onClick={() => handleSelectSession(session.id)}
className={cn(
"w-full rounded-lg border p-3 text-left transition-colors",
isActive
? "border-indigo-500 bg-zinc-50"
: "border-zinc-200 bg-zinc-100/50 hover:border-zinc-300 hover:bg-zinc-50",
)}
>
<div className="flex flex-col gap-1">
<Text
variant="body"
className={cn(
"font-medium",
isActive ? "text-indigo-900" : "text-zinc-900",
)}
>
{session.title || "Untitled Chat"}
</Text>
<div className="flex items-center gap-2 text-xs text-zinc-500">
<span>{session.id.slice(0, 8)}...</span>
{updatedAt && <span></span>}
<span>{updatedAt}</span>
</div>
</div>
</button>
);
})}
</div>
)}
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
}

View File

@@ -23,6 +23,7 @@ export function useChat() {
refreshSession,
claimSession,
clearSession: clearSessionBase,
loadSession,
} = useChatSession({
urlSessionId: null,
autoCreate: false,
@@ -112,6 +113,7 @@ export function useChat() {
createSession,
refreshSession,
clearSession,
loadSession,
sessionId: sessionIdFromHook,
};
}

View File

@@ -1,5 +1,6 @@
import {
getGetV2GetSessionQueryKey,
getGetV2GetSessionQueryOptions,
postV2CreateSession,
useGetV2GetSession,
usePatchV2SessionAssignUser,
@@ -156,8 +157,14 @@ export function useChatSession({
setError(null);
setSessionId(id);
storage.set(Key.CHAT_SESSION_ID, id);
const result = await refetch();
if (!result.data || result.isError) {
const queryOptions = getGetV2GetSessionQueryOptions(id, {
query: {
staleTime: Infinity,
retry: 1,
},
});
const result = await queryClient.fetchQuery(queryOptions);
if (!result || ("status" in result && result.status !== 200)) {
console.warn("Session not found on server, clearing local state");
storage.clean(Key.CHAT_SESSION_ID);
setSessionId(null);
@@ -170,7 +177,7 @@ export function useChatSession({
throw error;
}
},
[refetch],
[queryClient],
);
const refreshSession = useCallback(