Compare commits
7 Commits
add-llm-ma
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
524c17d1b6 | ||
|
|
9d4dcbd9e0 | ||
|
|
074be7aea6 | ||
|
|
39d28b24fc | ||
|
|
bf79a7748a | ||
|
|
649d4ab7f5 | ||
|
|
223df9d3da |
2
.github/workflows/classic-autogpt-ci.yml
vendored
@@ -139,7 +139,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload logs to artifact
|
- name: Upload logs to artifact
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: test-logs
|
name: test-logs
|
||||||
path: classic/original_autogpt/logs/
|
path: classic/original_autogpt/logs/
|
||||||
|
|||||||
2
.github/workflows/classic-forge-ci.yml
vendored
@@ -237,7 +237,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload logs to artifact
|
- name: Upload logs to artifact
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: test-logs
|
name: test-logs
|
||||||
path: classic/forge/logs/
|
path: classic/forge/logs/
|
||||||
|
|||||||
9
.github/workflows/platform-backend-ci.yml
vendored
@@ -41,13 +41,18 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
image: rabbitmq:3.12-management
|
image: rabbitmq:4.1.4
|
||||||
ports:
|
ports:
|
||||||
- 5672:5672
|
- 5672:5672
|
||||||
- 15672:15672
|
|
||||||
env:
|
env:
|
||||||
RABBITMQ_DEFAULT_USER: ${{ env.RABBITMQ_DEFAULT_USER }}
|
RABBITMQ_DEFAULT_USER: ${{ env.RABBITMQ_DEFAULT_USER }}
|
||||||
RABBITMQ_DEFAULT_PASS: ${{ env.RABBITMQ_DEFAULT_PASS }}
|
RABBITMQ_DEFAULT_PASS: ${{ env.RABBITMQ_DEFAULT_PASS }}
|
||||||
|
options: >-
|
||||||
|
--health-cmd "rabbitmq-diagnostics -q ping"
|
||||||
|
--health-interval 30s
|
||||||
|
--health-timeout 10s
|
||||||
|
--health-retries 5
|
||||||
|
--health-start-period 10s
|
||||||
clamav:
|
clamav:
|
||||||
image: clamav/clamav-debian:latest
|
image: clamav/clamav-debian:latest
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
10
.github/workflows/platform-frontend-ci.yml
vendored
@@ -6,10 +6,16 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- ".github/workflows/platform-frontend-ci.yml"
|
- ".github/workflows/platform-frontend-ci.yml"
|
||||||
- "autogpt_platform/frontend/**"
|
- "autogpt_platform/frontend/**"
|
||||||
|
- "autogpt_platform/backend/Dockerfile"
|
||||||
|
- "autogpt_platform/docker-compose.yml"
|
||||||
|
- "autogpt_platform/docker-compose.platform.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/platform-frontend-ci.yml"
|
- ".github/workflows/platform-frontend-ci.yml"
|
||||||
- "autogpt_platform/frontend/**"
|
- "autogpt_platform/frontend/**"
|
||||||
|
- "autogpt_platform/backend/Dockerfile"
|
||||||
|
- "autogpt_platform/docker-compose.yml"
|
||||||
|
- "autogpt_platform/docker-compose.platform.yml"
|
||||||
merge_group:
|
merge_group:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -263,7 +269,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Playwright report
|
- name: Upload Playwright report
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: playwright-report
|
||||||
path: playwright-report
|
path: playwright-report
|
||||||
@@ -272,7 +278,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Playwright test results
|
- name: Upload Playwright test results
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: playwright-test-results
|
name: playwright-test-results
|
||||||
path: test-results
|
path: test-results
|
||||||
|
|||||||
@@ -53,63 +53,6 @@ COPY autogpt_platform/backend/backend/data/partial_types.py ./backend/data/parti
|
|||||||
COPY autogpt_platform/backend/gen_prisma_types_stub.py ./
|
COPY autogpt_platform/backend/gen_prisma_types_stub.py ./
|
||||||
RUN poetry run prisma generate && poetry run gen-prisma-stub
|
RUN poetry run prisma generate && poetry run gen-prisma-stub
|
||||||
|
|
||||||
# ============================== BACKEND SERVER ============================== #
|
|
||||||
|
|
||||||
FROM debian:13-slim AS server
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV POETRY_HOME=/opt/poetry \
|
|
||||||
POETRY_NO_INTERACTION=1 \
|
|
||||||
POETRY_VIRTUALENVS_CREATE=true \
|
|
||||||
POETRY_VIRTUALENVS_IN_PROJECT=true \
|
|
||||||
DEBIAN_FRONTEND=noninteractive
|
|
||||||
ENV PATH=/opt/poetry/bin:$PATH
|
|
||||||
|
|
||||||
# Install Python, FFmpeg, ImageMagick, and CLI tools for agent use.
|
|
||||||
# bubblewrap provides OS-level sandbox (whitelist-only FS + no network)
|
|
||||||
# for the bash_exec MCP tool.
|
|
||||||
# Using --no-install-recommends saves ~650MB by skipping unnecessary deps like llvm, mesa, etc.
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
python3.13 \
|
|
||||||
python3-pip \
|
|
||||||
ffmpeg \
|
|
||||||
imagemagick \
|
|
||||||
jq \
|
|
||||||
ripgrep \
|
|
||||||
tree \
|
|
||||||
bubblewrap \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY --from=builder /usr/local/lib/python3* /usr/local/lib/python3*
|
|
||||||
COPY --from=builder /usr/local/bin/poetry /usr/local/bin/poetry
|
|
||||||
# Copy Node.js installation for Prisma
|
|
||||||
COPY --from=builder /usr/bin/node /usr/bin/node
|
|
||||||
COPY --from=builder /usr/lib/node_modules /usr/lib/node_modules
|
|
||||||
COPY --from=builder /usr/bin/npm /usr/bin/npm
|
|
||||||
COPY --from=builder /usr/bin/npx /usr/bin/npx
|
|
||||||
COPY --from=builder /root/.cache/prisma-python/binaries /root/.cache/prisma-python/binaries
|
|
||||||
|
|
||||||
WORKDIR /app/autogpt_platform/backend
|
|
||||||
|
|
||||||
# Copy only the .venv from builder (not the entire /app directory)
|
|
||||||
# The .venv includes the generated Prisma client
|
|
||||||
COPY --from=builder /app/autogpt_platform/backend/.venv ./.venv
|
|
||||||
ENV PATH="/app/autogpt_platform/backend/.venv/bin:$PATH"
|
|
||||||
|
|
||||||
# Copy dependency files + autogpt_libs (path dependency)
|
|
||||||
COPY autogpt_platform/autogpt_libs /app/autogpt_platform/autogpt_libs
|
|
||||||
COPY autogpt_platform/backend/poetry.lock autogpt_platform/backend/pyproject.toml ./
|
|
||||||
|
|
||||||
# Copy backend code + docs (for Copilot docs search)
|
|
||||||
COPY autogpt_platform/backend ./
|
|
||||||
COPY docs /app/docs
|
|
||||||
RUN poetry install --no-ansi --only-root
|
|
||||||
|
|
||||||
ENV PORT=8000
|
|
||||||
|
|
||||||
CMD ["poetry", "run", "rest"]
|
|
||||||
|
|
||||||
# =============================== DB MIGRATOR =============================== #
|
# =============================== DB MIGRATOR =============================== #
|
||||||
|
|
||||||
# Lightweight migrate stage - only needs Prisma CLI, not full Python environment
|
# Lightweight migrate stage - only needs Prisma CLI, not full Python environment
|
||||||
@@ -141,3 +84,59 @@ COPY autogpt_platform/backend/schema.prisma ./
|
|||||||
COPY autogpt_platform/backend/backend/data/partial_types.py ./backend/data/partial_types.py
|
COPY autogpt_platform/backend/backend/data/partial_types.py ./backend/data/partial_types.py
|
||||||
COPY autogpt_platform/backend/gen_prisma_types_stub.py ./
|
COPY autogpt_platform/backend/gen_prisma_types_stub.py ./
|
||||||
COPY autogpt_platform/backend/migrations ./migrations
|
COPY autogpt_platform/backend/migrations ./migrations
|
||||||
|
|
||||||
|
# ============================== BACKEND SERVER ============================== #
|
||||||
|
|
||||||
|
FROM debian:13-slim AS server
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Install Python, FFmpeg, ImageMagick, and CLI tools for agent use.
|
||||||
|
# bubblewrap provides OS-level sandbox (whitelist-only FS + no network)
|
||||||
|
# for the bash_exec MCP tool.
|
||||||
|
# Using --no-install-recommends saves ~650MB by skipping unnecessary deps like llvm, mesa, etc.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
python3.13 \
|
||||||
|
python3-pip \
|
||||||
|
ffmpeg \
|
||||||
|
imagemagick \
|
||||||
|
jq \
|
||||||
|
ripgrep \
|
||||||
|
tree \
|
||||||
|
bubblewrap \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy poetry (build-time only, for `poetry install --only-root` to create entry points)
|
||||||
|
COPY --from=builder /usr/local/lib/python3* /usr/local/lib/python3*
|
||||||
|
COPY --from=builder /usr/local/bin/poetry /usr/local/bin/poetry
|
||||||
|
# Copy Node.js installation for Prisma
|
||||||
|
COPY --from=builder /usr/bin/node /usr/bin/node
|
||||||
|
COPY --from=builder /usr/lib/node_modules /usr/lib/node_modules
|
||||||
|
COPY --from=builder /usr/bin/npm /usr/bin/npm
|
||||||
|
COPY --from=builder /usr/bin/npx /usr/bin/npx
|
||||||
|
COPY --from=builder /root/.cache/prisma-python/binaries /root/.cache/prisma-python/binaries
|
||||||
|
|
||||||
|
WORKDIR /app/autogpt_platform/backend
|
||||||
|
|
||||||
|
# Copy only the .venv from builder (not the entire /app directory)
|
||||||
|
# The .venv includes the generated Prisma client
|
||||||
|
COPY --from=builder /app/autogpt_platform/backend/.venv ./.venv
|
||||||
|
ENV PATH="/app/autogpt_platform/backend/.venv/bin:$PATH"
|
||||||
|
|
||||||
|
# Copy dependency files + autogpt_libs (path dependency)
|
||||||
|
COPY autogpt_platform/autogpt_libs /app/autogpt_platform/autogpt_libs
|
||||||
|
COPY autogpt_platform/backend/poetry.lock autogpt_platform/backend/pyproject.toml ./
|
||||||
|
|
||||||
|
# Copy backend code + docs (for Copilot docs search)
|
||||||
|
COPY autogpt_platform/backend ./
|
||||||
|
COPY docs /app/docs
|
||||||
|
# Install the project package to create entry point scripts in .venv/bin/
|
||||||
|
# (e.g., rest, executor, ws, db, scheduler, notification - see [tool.poetry.scripts])
|
||||||
|
RUN POETRY_VIRTUALENVS_CREATE=true POETRY_VIRTUALENVS_IN_PROJECT=true \
|
||||||
|
poetry install --no-ansi --only-root
|
||||||
|
|
||||||
|
ENV PORT=8000
|
||||||
|
|
||||||
|
CMD ["rest"]
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from .model import (
|
|||||||
ChatSession,
|
ChatSession,
|
||||||
append_and_save_message,
|
append_and_save_message,
|
||||||
create_chat_session,
|
create_chat_session,
|
||||||
|
delete_chat_session,
|
||||||
get_chat_session,
|
get_chat_session,
|
||||||
get_user_sessions,
|
get_user_sessions,
|
||||||
)
|
)
|
||||||
@@ -211,6 +212,43 @@ async def create_session(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/sessions/{session_id}",
|
||||||
|
dependencies=[Security(auth.requires_user)],
|
||||||
|
status_code=204,
|
||||||
|
responses={404: {"description": "Session not found or access denied"}},
|
||||||
|
)
|
||||||
|
async def delete_session(
|
||||||
|
session_id: str,
|
||||||
|
user_id: Annotated[str, Security(auth.get_user_id)],
|
||||||
|
) -> Response:
|
||||||
|
"""
|
||||||
|
Delete a chat session.
|
||||||
|
|
||||||
|
Permanently removes a chat session and all its messages.
|
||||||
|
Only the owner can delete their sessions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session_id: The session ID to delete.
|
||||||
|
user_id: The authenticated user's ID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
204 No Content on success.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: 404 if session not found or not owned by user.
|
||||||
|
"""
|
||||||
|
deleted = await delete_chat_session(session_id, user_id)
|
||||||
|
|
||||||
|
if not deleted:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Session {session_id} not found or access denied",
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(status_code=204)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/sessions/{session_id}",
|
"/sessions/{session_id}",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ services:
|
|||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
<<: *agpt-services
|
<<: *agpt-services
|
||||||
image: rabbitmq:management
|
image: rabbitmq:4.1.4
|
||||||
container_name: rabbitmq
|
container_name: rabbitmq
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: rabbitmq-diagnostics -q ping
|
test: rabbitmq-diagnostics -q ping
|
||||||
@@ -66,7 +66,6 @@ services:
|
|||||||
- RABBITMQ_DEFAULT_PASS=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7
|
- RABBITMQ_DEFAULT_PASS=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7
|
||||||
ports:
|
ports:
|
||||||
- "5672:5672"
|
- "5672:5672"
|
||||||
- "15672:15672"
|
|
||||||
clamav:
|
clamav:
|
||||||
image: clamav/clamav-debian:latest
|
image: clamav/clamav-debian:latest
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
image: rabbitmq:management
|
image: rabbitmq:4.1.4
|
||||||
container_name: rabbitmq
|
container_name: rabbitmq
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: rabbitmq-diagnostics -q ping
|
test: rabbitmq-diagnostics -q ping
|
||||||
@@ -88,14 +88,13 @@ services:
|
|||||||
<<: *backend-env
|
<<: *backend-env
|
||||||
ports:
|
ports:
|
||||||
- "5672:5672"
|
- "5672:5672"
|
||||||
- "15672:15672"
|
|
||||||
|
|
||||||
rest_server:
|
rest_server:
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: autogpt_platform/backend/Dockerfile
|
dockerfile: autogpt_platform/backend/Dockerfile
|
||||||
target: server
|
target: server
|
||||||
command: ["python", "-m", "backend.rest"]
|
command: ["rest"] # points to entry in [tool.poetry.scripts] in pyproject.toml
|
||||||
develop:
|
develop:
|
||||||
watch:
|
watch:
|
||||||
- path: ./
|
- path: ./
|
||||||
@@ -128,7 +127,7 @@ services:
|
|||||||
context: ../
|
context: ../
|
||||||
dockerfile: autogpt_platform/backend/Dockerfile
|
dockerfile: autogpt_platform/backend/Dockerfile
|
||||||
target: server
|
target: server
|
||||||
command: ["python", "-m", "backend.exec"]
|
command: ["executor"] # points to entry in [tool.poetry.scripts] in pyproject.toml
|
||||||
develop:
|
develop:
|
||||||
watch:
|
watch:
|
||||||
- path: ./
|
- path: ./
|
||||||
@@ -163,7 +162,7 @@ services:
|
|||||||
context: ../
|
context: ../
|
||||||
dockerfile: autogpt_platform/backend/Dockerfile
|
dockerfile: autogpt_platform/backend/Dockerfile
|
||||||
target: server
|
target: server
|
||||||
command: ["python", "-m", "backend.ws"]
|
command: ["ws"] # points to entry in [tool.poetry.scripts] in pyproject.toml
|
||||||
develop:
|
develop:
|
||||||
watch:
|
watch:
|
||||||
- path: ./
|
- path: ./
|
||||||
@@ -196,7 +195,7 @@ services:
|
|||||||
context: ../
|
context: ../
|
||||||
dockerfile: autogpt_platform/backend/Dockerfile
|
dockerfile: autogpt_platform/backend/Dockerfile
|
||||||
target: server
|
target: server
|
||||||
command: ["python", "-m", "backend.db"]
|
command: ["db"] # points to entry in [tool.poetry.scripts] in pyproject.toml
|
||||||
develop:
|
develop:
|
||||||
watch:
|
watch:
|
||||||
- path: ./
|
- path: ./
|
||||||
@@ -225,7 +224,7 @@ services:
|
|||||||
context: ../
|
context: ../
|
||||||
dockerfile: autogpt_platform/backend/Dockerfile
|
dockerfile: autogpt_platform/backend/Dockerfile
|
||||||
target: server
|
target: server
|
||||||
command: ["python", "-m", "backend.scheduler"]
|
command: ["scheduler"] # points to entry in [tool.poetry.scripts] in pyproject.toml
|
||||||
develop:
|
develop:
|
||||||
watch:
|
watch:
|
||||||
- path: ./
|
- path: ./
|
||||||
@@ -273,7 +272,7 @@ services:
|
|||||||
context: ../
|
context: ../
|
||||||
dockerfile: autogpt_platform/backend/Dockerfile
|
dockerfile: autogpt_platform/backend/Dockerfile
|
||||||
target: server
|
target: server
|
||||||
command: ["python", "-m", "backend.notification"]
|
command: ["notification"] # points to entry in [tool.poetry.scripts] in pyproject.toml
|
||||||
develop:
|
develop:
|
||||||
watch:
|
watch:
|
||||||
- path: ./
|
- path: ./
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||||
|
// TODO: Replace with modern Dialog component when available
|
||||||
|
import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog";
|
||||||
import { ChatContainer } from "./components/ChatContainer/ChatContainer";
|
import { ChatContainer } from "./components/ChatContainer/ChatContainer";
|
||||||
import { ChatSidebar } from "./components/ChatSidebar/ChatSidebar";
|
import { ChatSidebar } from "./components/ChatSidebar/ChatSidebar";
|
||||||
import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer";
|
import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer";
|
||||||
@@ -31,6 +33,12 @@ export function CopilotPage() {
|
|||||||
handleDrawerOpenChange,
|
handleDrawerOpenChange,
|
||||||
handleSelectSession,
|
handleSelectSession,
|
||||||
handleNewChat,
|
handleNewChat,
|
||||||
|
// Delete functionality
|
||||||
|
sessionToDelete,
|
||||||
|
isDeleting,
|
||||||
|
handleDeleteClick,
|
||||||
|
handleConfirmDelete,
|
||||||
|
handleCancelDelete,
|
||||||
} = useCopilotPage();
|
} = useCopilotPage();
|
||||||
|
|
||||||
if (isUserLoading || !isLoggedIn) {
|
if (isUserLoading || !isLoggedIn) {
|
||||||
@@ -48,7 +56,19 @@ export function CopilotPage() {
|
|||||||
>
|
>
|
||||||
{!isMobile && <ChatSidebar />}
|
{!isMobile && <ChatSidebar />}
|
||||||
<div className="relative flex h-full w-full flex-col overflow-hidden bg-[#f8f8f9] px-0">
|
<div className="relative flex h-full w-full flex-col overflow-hidden bg-[#f8f8f9] px-0">
|
||||||
{isMobile && <MobileHeader onOpenDrawer={handleOpenDrawer} />}
|
{isMobile && (
|
||||||
|
<MobileHeader
|
||||||
|
onOpenDrawer={handleOpenDrawer}
|
||||||
|
showDelete={!!sessionId}
|
||||||
|
isDeleting={isDeleting}
|
||||||
|
onDelete={() => {
|
||||||
|
const session = sessions.find((s) => s.id === sessionId);
|
||||||
|
if (session) {
|
||||||
|
handleDeleteClick(session.id, session.title);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<ChatContainer
|
<ChatContainer
|
||||||
messages={messages}
|
messages={messages}
|
||||||
@@ -75,6 +95,16 @@ export function CopilotPage() {
|
|||||||
onOpenChange={handleDrawerOpenChange}
|
onOpenChange={handleDrawerOpenChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{/* Delete confirmation dialog - rendered at top level for proper z-index on mobile */}
|
||||||
|
{isMobile && (
|
||||||
|
<DeleteConfirmDialog
|
||||||
|
entityType="chat"
|
||||||
|
entityName={sessionToDelete?.title || "Untitled chat"}
|
||||||
|
open={!!sessionToDelete}
|
||||||
|
onOpenChange={(open) => !open && handleCancelDelete()}
|
||||||
|
onDoDelete={handleConfirmDelete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat";
|
import {
|
||||||
|
getGetV2ListSessionsQueryKey,
|
||||||
|
useDeleteV2DeleteSession,
|
||||||
|
useGetV2ListSessions,
|
||||||
|
} from "@/app/api/__generated__/endpoints/chat/chat";
|
||||||
import { Button } from "@/components/atoms/Button/Button";
|
import { Button } from "@/components/atoms/Button/Button";
|
||||||
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
||||||
import { Text } from "@/components/atoms/Text/Text";
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
|
import { toast } from "@/components/molecules/Toast/use-toast";
|
||||||
|
// TODO: Replace with modern Dialog component when available
|
||||||
|
import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog";
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -12,18 +19,52 @@ import {
|
|||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PlusCircleIcon, PlusIcon } from "@phosphor-icons/react";
|
import { PlusCircleIcon, PlusIcon, TrashIcon } from "@phosphor-icons/react";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import { useState } from "react";
|
||||||
import { parseAsString, useQueryState } from "nuqs";
|
import { parseAsString, useQueryState } from "nuqs";
|
||||||
|
|
||||||
export function ChatSidebar() {
|
export function ChatSidebar() {
|
||||||
const { state } = useSidebar();
|
const { state } = useSidebar();
|
||||||
const isCollapsed = state === "collapsed";
|
const isCollapsed = state === "collapsed";
|
||||||
const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString);
|
const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString);
|
||||||
|
const [sessionToDelete, setSessionToDelete] = useState<{
|
||||||
|
id: string;
|
||||||
|
title: string | null | undefined;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { data: sessionsResponse, isLoading: isLoadingSessions } =
|
const { data: sessionsResponse, isLoading: isLoadingSessions } =
|
||||||
useGetV2ListSessions({ limit: 50 });
|
useGetV2ListSessions({ limit: 50 });
|
||||||
|
|
||||||
|
const { mutate: deleteSession, isPending: isDeleting } =
|
||||||
|
useDeleteV2DeleteSession({
|
||||||
|
mutation: {
|
||||||
|
onSuccess: () => {
|
||||||
|
// Invalidate sessions list to refetch
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: getGetV2ListSessionsQueryKey(),
|
||||||
|
});
|
||||||
|
// If we deleted the current session, clear selection
|
||||||
|
if (sessionToDelete?.id === sessionId) {
|
||||||
|
setSessionId(null);
|
||||||
|
}
|
||||||
|
setSessionToDelete(null);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Failed to delete chat",
|
||||||
|
description:
|
||||||
|
error instanceof Error ? error.message : "An error occurred",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setSessionToDelete(null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const sessions =
|
const sessions =
|
||||||
sessionsResponse?.status === 200 ? sessionsResponse.data.sessions : [];
|
sessionsResponse?.status === 200 ? sessionsResponse.data.sessions : [];
|
||||||
|
|
||||||
@@ -35,6 +76,22 @@ export function ChatSidebar() {
|
|||||||
setSessionId(id);
|
setSessionId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDeleteClick(
|
||||||
|
e: React.MouseEvent,
|
||||||
|
id: string,
|
||||||
|
title: string | null | undefined,
|
||||||
|
) {
|
||||||
|
e.stopPropagation(); // Prevent session selection
|
||||||
|
if (isDeleting) return; // Prevent double-click during deletion
|
||||||
|
setSessionToDelete({ id, title });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfirmDelete() {
|
||||||
|
if (sessionToDelete) {
|
||||||
|
deleteSession({ sessionId: sessionToDelete.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(dateString: string) {
|
function formatDate(dateString: string) {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -61,128 +118,152 @@ export function ChatSidebar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar
|
<>
|
||||||
variant="inset"
|
<Sidebar
|
||||||
collapsible="icon"
|
variant="inset"
|
||||||
className="!top-[50px] !h-[calc(100vh-50px)] border-r border-zinc-100 px-0"
|
collapsible="icon"
|
||||||
>
|
className="!top-[50px] !h-[calc(100vh-50px)] border-r border-zinc-100 px-0"
|
||||||
{isCollapsed && (
|
>
|
||||||
<SidebarHeader
|
{isCollapsed && (
|
||||||
className={cn(
|
<SidebarHeader
|
||||||
"flex",
|
className={cn(
|
||||||
isCollapsed
|
"flex",
|
||||||
? "flex-row items-center justify-between gap-y-4 md:flex-col md:items-start md:justify-start"
|
isCollapsed
|
||||||
: "flex-row items-center justify-between",
|
? "flex-row items-center justify-between gap-y-4 md:flex-col md:items-start md:justify-start"
|
||||||
)}
|
: "flex-row items-center justify-between",
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
key={isCollapsed ? "header-collapsed" : "header-expanded"}
|
|
||||||
className="flex flex-col items-center gap-3 pt-4"
|
|
||||||
initial={{ opacity: 0, filter: "blur(3px)" }}
|
|
||||||
animate={{ opacity: 1, filter: "blur(0px)" }}
|
|
||||||
transition={{ type: "spring", bounce: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
|
||||||
<SidebarTrigger />
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={handleNewChat}
|
|
||||||
style={{ minWidth: "auto", width: "auto" }}
|
|
||||||
>
|
|
||||||
<PlusCircleIcon className="!size-5" />
|
|
||||||
<span className="sr-only">New Chat</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</SidebarHeader>
|
|
||||||
)}
|
|
||||||
<SidebarContent className="gap-4 overflow-y-auto px-4 py-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
|
||||||
{!isCollapsed && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ duration: 0.2, delay: 0.1 }}
|
|
||||||
className="flex items-center justify-between px-3"
|
|
||||||
>
|
|
||||||
<Text variant="h3" size="body-medium">
|
|
||||||
Your chats
|
|
||||||
</Text>
|
|
||||||
<div className="relative left-6">
|
|
||||||
<SidebarTrigger />
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isCollapsed && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ duration: 0.2, delay: 0.15 }}
|
|
||||||
className="mt-4 flex flex-col gap-1"
|
|
||||||
>
|
|
||||||
{isLoadingSessions ? (
|
|
||||||
<div className="flex min-h-[30rem] items-center justify-center py-4">
|
|
||||||
<LoadingSpinner size="small" className="text-neutral-600" />
|
|
||||||
</div>
|
|
||||||
) : sessions.length === 0 ? (
|
|
||||||
<p className="py-4 text-center text-sm text-neutral-500">
|
|
||||||
No conversations yet
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
sessions.map((session) => (
|
|
||||||
<button
|
|
||||||
key={session.id}
|
|
||||||
onClick={() => handleSelectSession(session.id)}
|
|
||||||
className={cn(
|
|
||||||
"w-full rounded-lg px-3 py-2.5 text-left transition-colors",
|
|
||||||
session.id === sessionId
|
|
||||||
? "bg-zinc-100"
|
|
||||||
: "hover:bg-zinc-50",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex min-w-0 max-w-full flex-col overflow-hidden">
|
|
||||||
<div className="min-w-0 max-w-full">
|
|
||||||
<Text
|
|
||||||
variant="body"
|
|
||||||
className={cn(
|
|
||||||
"truncate font-normal",
|
|
||||||
session.id === sessionId
|
|
||||||
? "text-zinc-600"
|
|
||||||
: "text-zinc-800",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{session.title || `Untitled chat`}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<Text variant="small" className="text-neutral-400">
|
|
||||||
{formatDate(session.updated_at)}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</SidebarContent>
|
|
||||||
{!isCollapsed && sessionId && (
|
|
||||||
<SidebarFooter className="shrink-0 bg-zinc-50 p-3 pb-1 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ duration: 0.2, delay: 0.2 }}
|
|
||||||
>
|
>
|
||||||
<Button
|
<motion.div
|
||||||
variant="primary"
|
key={isCollapsed ? "header-collapsed" : "header-expanded"}
|
||||||
size="small"
|
className="flex flex-col items-center gap-3 pt-4"
|
||||||
onClick={handleNewChat}
|
initial={{ opacity: 0, filter: "blur(3px)" }}
|
||||||
className="w-full"
|
animate={{ opacity: 1, filter: "blur(0px)" }}
|
||||||
leftIcon={<PlusIcon className="h-4 w-4" weight="bold" />}
|
transition={{ type: "spring", bounce: 0.2 }}
|
||||||
>
|
>
|
||||||
New Chat
|
<div className="flex flex-col items-center gap-2">
|
||||||
</Button>
|
<SidebarTrigger />
|
||||||
</motion.div>
|
<Button
|
||||||
</SidebarFooter>
|
variant="ghost"
|
||||||
)}
|
onClick={handleNewChat}
|
||||||
</Sidebar>
|
style={{ minWidth: "auto", width: "auto" }}
|
||||||
|
>
|
||||||
|
<PlusCircleIcon className="!size-5" />
|
||||||
|
<span className="sr-only">New Chat</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</SidebarHeader>
|
||||||
|
)}
|
||||||
|
<SidebarContent className="gap-4 overflow-y-auto px-4 py-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||||
|
{!isCollapsed && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.2, delay: 0.1 }}
|
||||||
|
className="flex items-center justify-between px-3"
|
||||||
|
>
|
||||||
|
<Text variant="h3" size="body-medium">
|
||||||
|
Your chats
|
||||||
|
</Text>
|
||||||
|
<div className="relative left-6">
|
||||||
|
<SidebarTrigger />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.2, delay: 0.15 }}
|
||||||
|
className="mt-4 flex flex-col gap-1"
|
||||||
|
>
|
||||||
|
{isLoadingSessions ? (
|
||||||
|
<div className="flex min-h-[30rem] items-center justify-center py-4">
|
||||||
|
<LoadingSpinner size="small" className="text-neutral-600" />
|
||||||
|
</div>
|
||||||
|
) : sessions.length === 0 ? (
|
||||||
|
<p className="py-4 text-center text-sm text-neutral-500">
|
||||||
|
No conversations yet
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
sessions.map((session) => (
|
||||||
|
<div
|
||||||
|
key={session.id}
|
||||||
|
className={cn(
|
||||||
|
"group relative w-full rounded-lg transition-colors",
|
||||||
|
session.id === sessionId
|
||||||
|
? "bg-zinc-100"
|
||||||
|
: "hover:bg-zinc-50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSelectSession(session.id)}
|
||||||
|
className="w-full px-3 py-2.5 pr-10 text-left"
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 max-w-full flex-col overflow-hidden">
|
||||||
|
<div className="min-w-0 max-w-full">
|
||||||
|
<Text
|
||||||
|
variant="body"
|
||||||
|
className={cn(
|
||||||
|
"truncate font-normal",
|
||||||
|
session.id === sessionId
|
||||||
|
? "text-zinc-600"
|
||||||
|
: "text-zinc-800",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{session.title || `Untitled chat`}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Text variant="small" className="text-neutral-400">
|
||||||
|
{formatDate(session.updated_at)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) =>
|
||||||
|
handleDeleteClick(e, session.id, session.title)
|
||||||
|
}
|
||||||
|
disabled={isDeleting}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1.5 text-zinc-400 opacity-0 transition-all group-hover:opacity-100 hover:bg-red-100 hover:text-red-600 focus-visible:opacity-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
aria-label="Delete chat"
|
||||||
|
>
|
||||||
|
<TrashIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</SidebarContent>
|
||||||
|
{!isCollapsed && sessionId && (
|
||||||
|
<SidebarFooter className="shrink-0 bg-zinc-50 p-3 pb-1 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.2, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={handleNewChat}
|
||||||
|
className="w-full"
|
||||||
|
leftIcon={<PlusIcon className="h-4 w-4" weight="bold" />}
|
||||||
|
>
|
||||||
|
New Chat
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</SidebarFooter>
|
||||||
|
)}
|
||||||
|
</Sidebar>
|
||||||
|
|
||||||
|
<DeleteConfirmDialog
|
||||||
|
entityType="chat"
|
||||||
|
entityName={sessionToDelete?.title || "Untitled chat"}
|
||||||
|
open={!!sessionToDelete}
|
||||||
|
onOpenChange={(open) => !open && setSessionToDelete(null)}
|
||||||
|
onDoDelete={handleConfirmDelete}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,46 @@
|
|||||||
import { Button } from "@/components/atoms/Button/Button";
|
import { Button } from "@/components/atoms/Button/Button";
|
||||||
import { NAVBAR_HEIGHT_PX } from "@/lib/constants";
|
import { NAVBAR_HEIGHT_PX } from "@/lib/constants";
|
||||||
import { ListIcon } from "@phosphor-icons/react";
|
import { ListIcon, TrashIcon } from "@phosphor-icons/react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onOpenDrawer: () => void;
|
onOpenDrawer: () => void;
|
||||||
|
showDelete?: boolean;
|
||||||
|
isDeleting?: boolean;
|
||||||
|
onDelete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MobileHeader({ onOpenDrawer }: Props) {
|
export function MobileHeader({
|
||||||
|
onOpenDrawer,
|
||||||
|
showDelete,
|
||||||
|
isDeleting,
|
||||||
|
onDelete,
|
||||||
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<div
|
||||||
variant="icon"
|
className="fixed z-50 flex gap-2"
|
||||||
size="icon"
|
|
||||||
aria-label="Open sessions"
|
|
||||||
onClick={onOpenDrawer}
|
|
||||||
className="fixed z-50 bg-white shadow-md"
|
|
||||||
style={{ left: "1rem", top: `${NAVBAR_HEIGHT_PX + 20}px` }}
|
style={{ left: "1rem", top: `${NAVBAR_HEIGHT_PX + 20}px` }}
|
||||||
>
|
>
|
||||||
<ListIcon width="1.25rem" height="1.25rem" />
|
<Button
|
||||||
</Button>
|
variant="icon"
|
||||||
|
size="icon"
|
||||||
|
aria-label="Open sessions"
|
||||||
|
onClick={onOpenDrawer}
|
||||||
|
className="bg-white shadow-md"
|
||||||
|
>
|
||||||
|
<ListIcon width="1.25rem" height="1.25rem" />
|
||||||
|
</Button>
|
||||||
|
{showDelete && onDelete && (
|
||||||
|
<Button
|
||||||
|
variant="icon"
|
||||||
|
size="icon"
|
||||||
|
aria-label="Delete current chat"
|
||||||
|
onClick={onDelete}
|
||||||
|
disabled={isDeleting}
|
||||||
|
className="bg-white text-red-500 shadow-md hover:bg-red-50 hover:text-red-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<TrashIcon width="1.25rem" height="1.25rem" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { Button } from "@/components/atoms/Button/Button";
|
|||||||
import { Text } from "@/components/atoms/Text/Text";
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
import {
|
import {
|
||||||
BookOpenIcon,
|
BookOpenIcon,
|
||||||
CheckFatIcon,
|
|
||||||
PencilSimpleIcon,
|
PencilSimpleIcon,
|
||||||
WarningDiamondIcon,
|
WarningDiamondIcon,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import type { ToolUIPart } from "ai";
|
import type { ToolUIPart } from "ai";
|
||||||
|
import Image from "next/image";
|
||||||
import NextLink from "next/link";
|
import NextLink from "next/link";
|
||||||
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
ClarificationQuestionsCard,
|
ClarificationQuestionsCard,
|
||||||
ClarifyingQuestion,
|
ClarifyingQuestion,
|
||||||
} from "./components/ClarificationQuestionsCard";
|
} from "./components/ClarificationQuestionsCard";
|
||||||
|
import sparklesImg from "./components/MiniGame/assets/sparkles.png";
|
||||||
import { MiniGame } from "./components/MiniGame/MiniGame";
|
import { MiniGame } from "./components/MiniGame/MiniGame";
|
||||||
import {
|
import {
|
||||||
AccordionIcon,
|
AccordionIcon,
|
||||||
@@ -83,7 +84,8 @@ function getAccordionMeta(output: CreateAgentToolOutput) {
|
|||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
icon,
|
icon,
|
||||||
title: "Creating agent, this may take a few minutes. Sit back and relax.",
|
title:
|
||||||
|
"Creating agent, this may take a few minutes. Play while you wait.",
|
||||||
expanded: true,
|
expanded: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -167,16 +169,20 @@ export function CreateAgentTool({ part }: Props) {
|
|||||||
{isAgentSavedOutput(output) && (
|
{isAgentSavedOutput(output) && (
|
||||||
<div className="rounded-xl border border-border/60 bg-card p-4 shadow-sm">
|
<div className="rounded-xl border border-border/60 bg-card p-4 shadow-sm">
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex items-baseline gap-2">
|
||||||
<CheckFatIcon
|
<Image
|
||||||
size={18}
|
src={sparklesImg}
|
||||||
weight="regular"
|
alt="sparkles"
|
||||||
className="relative top-1 text-green-500"
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className="relative top-1"
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
variant="body-medium"
|
variant="body-medium"
|
||||||
className="text-blacks mb-2 text-[16px]"
|
className="mb-2 text-[16px] text-black"
|
||||||
>
|
>
|
||||||
{output.message}
|
Agent{" "}
|
||||||
|
<span className="text-violet-600">{output.agent_name}</span>{" "}
|
||||||
|
has been saved to your library!
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 flex flex-wrap gap-4">
|
<div className="mt-3 flex flex-wrap gap-4">
|
||||||
|
|||||||
@@ -2,20 +2,65 @@
|
|||||||
|
|
||||||
import { useMiniGame } from "./useMiniGame";
|
import { useMiniGame } from "./useMiniGame";
|
||||||
|
|
||||||
|
function Key({ children }: { children: React.ReactNode }) {
|
||||||
|
return <strong>[{children}]</strong>;
|
||||||
|
}
|
||||||
|
|
||||||
export function MiniGame() {
|
export function MiniGame() {
|
||||||
const { canvasRef } = useMiniGame();
|
const { canvasRef, activeMode, showOverlay, score, highScore, onContinue } =
|
||||||
|
useMiniGame();
|
||||||
|
|
||||||
|
const isRunActive =
|
||||||
|
activeMode === "run" || activeMode === "idle" || activeMode === "over";
|
||||||
|
|
||||||
|
let overlayText: string | undefined;
|
||||||
|
let buttonLabel = "Continue";
|
||||||
|
if (activeMode === "idle") {
|
||||||
|
buttonLabel = "Start";
|
||||||
|
} else if (activeMode === "boss-intro") {
|
||||||
|
overlayText = "Face the bandit!";
|
||||||
|
} else if (activeMode === "boss-defeated") {
|
||||||
|
overlayText = "Great job, keep on going";
|
||||||
|
} else if (activeMode === "over") {
|
||||||
|
overlayText = `Score: ${score} / Record: ${highScore}`;
|
||||||
|
buttonLabel = "Retry";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex flex-col gap-2">
|
||||||
className="w-full overflow-hidden rounded-md bg-background text-foreground"
|
<p className="text-sm font-medium text-purple-500">
|
||||||
style={{ border: "1px solid #d17fff" }}
|
{isRunActive ? (
|
||||||
>
|
<>
|
||||||
<canvas
|
Run mode: <Key>Space</Key> to jump
|
||||||
ref={canvasRef}
|
</>
|
||||||
tabIndex={0}
|
) : (
|
||||||
className="block w-full outline-none"
|
<>
|
||||||
style={{ imageRendering: "pixelated" }}
|
Duel mode: <Key>←→</Key> to move · <Key>Z</Key> to attack ·{" "}
|
||||||
/>
|
<Key>X</Key> to block · <Key>Space</Key> to jump
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div className="relative w-full overflow-hidden rounded-md border border-accent bg-background text-foreground">
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
tabIndex={0}
|
||||||
|
className="block w-full outline-none"
|
||||||
|
/>
|
||||||
|
{showOverlay && (
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/40">
|
||||||
|
{overlayText && (
|
||||||
|
<p className="text-lg font-bold text-white">{overlayText}</p>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onContinue}
|
||||||
|
className="rounded-md bg-white px-4 py-2 text-sm font-semibold text-zinc-800 shadow-md transition-colors hover:bg-zinc-100"
|
||||||
|
>
|
||||||
|
{buttonLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -136,7 +136,7 @@ export function getAnimationText(part: {
|
|||||||
if (isOperationPendingOutput(output)) return "Agent creation in progress";
|
if (isOperationPendingOutput(output)) return "Agent creation in progress";
|
||||||
if (isOperationInProgressOutput(output))
|
if (isOperationInProgressOutput(output))
|
||||||
return "Agent creation already in progress";
|
return "Agent creation already in progress";
|
||||||
if (isAgentSavedOutput(output)) return `Saved "${output.agent_name}"`;
|
if (isAgentSavedOutput(output)) return `Saved ${output.agent_name}`;
|
||||||
if (isAgentPreviewOutput(output)) return `Preview "${output.agent_name}"`;
|
if (isAgentPreviewOutput(output)) return `Preview "${output.agent_name}"`;
|
||||||
if (isClarificationNeededOutput(output)) return "Needs clarification";
|
if (isClarificationNeededOutput(output)) return "Needs clarification";
|
||||||
return "Error creating agent";
|
return "Error creating agent";
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type { ToolUIPart } from "ai";
|
|||||||
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||||
import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
|
import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
|
||||||
import { ProgressBar } from "../../components/ProgressBar/ProgressBar";
|
|
||||||
import {
|
import {
|
||||||
ContentCardDescription,
|
ContentCardDescription,
|
||||||
ContentCodeBlock,
|
ContentCodeBlock,
|
||||||
@@ -15,7 +14,7 @@ import {
|
|||||||
ContentMessage,
|
ContentMessage,
|
||||||
} from "../../components/ToolAccordion/AccordionContent";
|
} from "../../components/ToolAccordion/AccordionContent";
|
||||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||||
import { useAsymptoticProgress } from "../../hooks/useAsymptoticProgress";
|
import { MiniGame } from "../CreateAgent/components/MiniGame/MiniGame";
|
||||||
import {
|
import {
|
||||||
ClarificationQuestionsCard,
|
ClarificationQuestionsCard,
|
||||||
ClarifyingQuestion,
|
ClarifyingQuestion,
|
||||||
@@ -54,6 +53,7 @@ function getAccordionMeta(output: EditAgentToolOutput): {
|
|||||||
title: string;
|
title: string;
|
||||||
titleClassName?: string;
|
titleClassName?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
expanded?: boolean;
|
||||||
} {
|
} {
|
||||||
const icon = <AccordionIcon />;
|
const icon = <AccordionIcon />;
|
||||||
|
|
||||||
@@ -80,7 +80,11 @@ function getAccordionMeta(output: EditAgentToolOutput): {
|
|||||||
isOperationPendingOutput(output) ||
|
isOperationPendingOutput(output) ||
|
||||||
isOperationInProgressOutput(output)
|
isOperationInProgressOutput(output)
|
||||||
) {
|
) {
|
||||||
return { icon: <OrbitLoader size={32} />, title: "Editing agent" };
|
return {
|
||||||
|
icon: <OrbitLoader size={32} />,
|
||||||
|
title: "Editing agent, this may take a few minutes. Play while you wait.",
|
||||||
|
expanded: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
icon: (
|
icon: (
|
||||||
@@ -105,7 +109,6 @@ export function EditAgentTool({ part }: Props) {
|
|||||||
(isOperationStartedOutput(output) ||
|
(isOperationStartedOutput(output) ||
|
||||||
isOperationPendingOutput(output) ||
|
isOperationPendingOutput(output) ||
|
||||||
isOperationInProgressOutput(output));
|
isOperationInProgressOutput(output));
|
||||||
const progress = useAsymptoticProgress(isOperating);
|
|
||||||
const hasExpandableContent =
|
const hasExpandableContent =
|
||||||
part.state === "output-available" &&
|
part.state === "output-available" &&
|
||||||
!!output &&
|
!!output &&
|
||||||
@@ -149,9 +152,9 @@ export function EditAgentTool({ part }: Props) {
|
|||||||
<ToolAccordion {...getAccordionMeta(output)}>
|
<ToolAccordion {...getAccordionMeta(output)}>
|
||||||
{isOperating && (
|
{isOperating && (
|
||||||
<ContentGrid>
|
<ContentGrid>
|
||||||
<ProgressBar value={progress} className="max-w-[280px]" />
|
<MiniGame />
|
||||||
<ContentHint>
|
<ContentHint>
|
||||||
This could take a few minutes, grab a coffee ☕
|
This could take a few minutes — play while you wait!
|
||||||
</ContentHint>
|
</ContentHint>
|
||||||
</ContentGrid>
|
</ContentGrid>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,8 +2,14 @@
|
|||||||
|
|
||||||
import type { ToolUIPart } from "ai";
|
import type { ToolUIPart } from "ai";
|
||||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||||
|
import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
|
||||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||||
import { ContentMessage } from "../../components/ToolAccordion/AccordionContent";
|
import {
|
||||||
|
ContentGrid,
|
||||||
|
ContentHint,
|
||||||
|
ContentMessage,
|
||||||
|
} from "../../components/ToolAccordion/AccordionContent";
|
||||||
|
import { MiniGame } from "../CreateAgent/components/MiniGame/MiniGame";
|
||||||
import {
|
import {
|
||||||
getAccordionMeta,
|
getAccordionMeta,
|
||||||
getAnimationText,
|
getAnimationText,
|
||||||
@@ -60,6 +66,21 @@ export function RunAgentTool({ part }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isStreaming && !output && (
|
||||||
|
<ToolAccordion
|
||||||
|
icon={<OrbitLoader size={32} />}
|
||||||
|
title="Running agent, this may take a few minutes. Play while you wait."
|
||||||
|
expanded={true}
|
||||||
|
>
|
||||||
|
<ContentGrid>
|
||||||
|
<MiniGame />
|
||||||
|
<ContentHint>
|
||||||
|
This could take a few minutes — play while you wait!
|
||||||
|
</ContentHint>
|
||||||
|
</ContentGrid>
|
||||||
|
</ToolAccordion>
|
||||||
|
)}
|
||||||
|
|
||||||
{hasExpandableContent && output && (
|
{hasExpandableContent && output && (
|
||||||
<ToolAccordion {...getAccordionMeta(output)}>
|
<ToolAccordion {...getAccordionMeta(output)}>
|
||||||
{isRunAgentExecutionStartedOutput(output) && (
|
{isRunAgentExecutionStartedOutput(output) && (
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat";
|
import {
|
||||||
|
getGetV2ListSessionsQueryKey,
|
||||||
|
useDeleteV2DeleteSession,
|
||||||
|
useGetV2ListSessions,
|
||||||
|
} from "@/app/api/__generated__/endpoints/chat/chat";
|
||||||
import { toast } from "@/components/molecules/Toast/use-toast";
|
import { toast } from "@/components/molecules/Toast/use-toast";
|
||||||
import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
|
import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
|
||||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||||
import { useChat } from "@ai-sdk/react";
|
import { useChat } from "@ai-sdk/react";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { DefaultChatTransport } from "ai";
|
import { DefaultChatTransport } from "ai";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useChatSession } from "./useChatSession";
|
import { useChatSession } from "./useChatSession";
|
||||||
import { useLongRunningToolPolling } from "./hooks/useLongRunningToolPolling";
|
import { useLongRunningToolPolling } from "./hooks/useLongRunningToolPolling";
|
||||||
|
|
||||||
@@ -14,6 +19,11 @@ export function useCopilotPage() {
|
|||||||
const { isUserLoading, isLoggedIn } = useSupabase();
|
const { isUserLoading, isLoggedIn } = useSupabase();
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [pendingMessage, setPendingMessage] = useState<string | null>(null);
|
const [pendingMessage, setPendingMessage] = useState<string | null>(null);
|
||||||
|
const [sessionToDelete, setSessionToDelete] = useState<{
|
||||||
|
id: string;
|
||||||
|
title: string | null | undefined;
|
||||||
|
} | null>(null);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -24,6 +34,30 @@ export function useCopilotPage() {
|
|||||||
isCreatingSession,
|
isCreatingSession,
|
||||||
} = useChatSession();
|
} = useChatSession();
|
||||||
|
|
||||||
|
const { mutate: deleteSessionMutation, isPending: isDeleting } =
|
||||||
|
useDeleteV2DeleteSession({
|
||||||
|
mutation: {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: getGetV2ListSessionsQueryKey(),
|
||||||
|
});
|
||||||
|
if (sessionToDelete?.id === sessionId) {
|
||||||
|
setSessionId(null);
|
||||||
|
}
|
||||||
|
setSessionToDelete(null);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Failed to delete chat",
|
||||||
|
description:
|
||||||
|
error instanceof Error ? error.message : "An error occurred",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setSessionToDelete(null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const breakpoint = useBreakpoint();
|
const breakpoint = useBreakpoint();
|
||||||
const isMobile =
|
const isMobile =
|
||||||
breakpoint === "base" || breakpoint === "sm" || breakpoint === "md";
|
breakpoint === "base" || breakpoint === "sm" || breakpoint === "md";
|
||||||
@@ -143,6 +177,24 @@ export function useCopilotPage() {
|
|||||||
if (isMobile) setIsDrawerOpen(false);
|
if (isMobile) setIsDrawerOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDeleteClick = useCallback(
|
||||||
|
(id: string, title: string | null | undefined) => {
|
||||||
|
if (isDeleting) return;
|
||||||
|
setSessionToDelete({ id, title });
|
||||||
|
},
|
||||||
|
[isDeleting],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConfirmDelete = useCallback(() => {
|
||||||
|
if (sessionToDelete) {
|
||||||
|
deleteSessionMutation({ sessionId: sessionToDelete.id });
|
||||||
|
}
|
||||||
|
}, [sessionToDelete, deleteSessionMutation]);
|
||||||
|
|
||||||
|
const handleCancelDelete = useCallback(() => {
|
||||||
|
setSessionToDelete(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessionId,
|
sessionId,
|
||||||
messages,
|
messages,
|
||||||
@@ -165,5 +217,11 @@ export function useCopilotPage() {
|
|||||||
handleDrawerOpenChange,
|
handleDrawerOpenChange,
|
||||||
handleSelectSession,
|
handleSelectSession,
|
||||||
handleNewChat,
|
handleNewChat,
|
||||||
|
// Delete functionality
|
||||||
|
sessionToDelete,
|
||||||
|
isDeleting,
|
||||||
|
handleDeleteClick,
|
||||||
|
handleConfirmDelete,
|
||||||
|
handleCancelDelete,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1151,6 +1151,36 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/chat/sessions/{session_id}": {
|
"/api/chat/sessions/{session_id}": {
|
||||||
|
"delete": {
|
||||||
|
"tags": ["v2", "chat", "chat"],
|
||||||
|
"summary": "Delete Session",
|
||||||
|
"description": "Delete a chat session.\n\nPermanently removes a chat session and all its messages.\nOnly the owner can delete their sessions.\n\nArgs:\n session_id: The session ID to delete.\n user_id: The authenticated user's ID.\n\nReturns:\n 204 No Content on success.\n\nRaises:\n HTTPException: 404 if session not found or not owned by user.",
|
||||||
|
"operationId": "deleteV2DeleteSession",
|
||||||
|
"security": [{ "HTTPBearerJWT": [] }],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "session_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": { "type": "string", "title": "Session Id" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": { "description": "Successful Response" },
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
|
||||||
|
},
|
||||||
|
"404": { "description": "Session not found or access denied" },
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"get": {
|
"get": {
|
||||||
"tags": ["v2", "chat", "chat"],
|
"tags": ["v2", "chat", "chat"],
|
||||||
"summary": "Get Session",
|
"summary": "Get Session",
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const DialogFooter = ({
|
|||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
4
autogpt_platform/frontend/src/types/images.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
declare module "*.png" {
|
||||||
|
const content: import("next/image").StaticImageData;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||