Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/web/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
node_modules
dist
.next
.turbo
*.log
.env
.env.local
.env.*.local
coverage
*.test.ts
*.spec.ts
__tests__
.DS_Store
*.md
.git
.gitignore
.prettierrc
.eslintrc
biome.json
58 changes: 58 additions & 0 deletions apps/web/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Build stage
FROM node:22-alpine AS builder

WORKDIR /app

# Install pnpm globally (project uses pnpm)
RUN npm install -g pnpm

# Copy all package files for dependency resolution
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./

# Copy all packages and apps
COPY packages/ ./packages/
COPY apps/ ./apps/
COPY turbo.json ./

# Install all dependencies including workspace packages
RUN pnpm install --frozen-lockfile

# Build the web application with standalone output
ENV NEXT_CONFIG_OUTPUT=standalone
RUN pnpm --filter @openzosma/web build

# Production stage
FROM node:22-alpine AS runner

WORKDIR /app

# Set environment to production
ENV NODE_ENV=production

# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001

# Copy necessary files from builder
COPY --from=builder /app/apps/web/public ./apps/web/public
COPY --from=builder /app/apps/web/.next/standalone ./
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static

# Set correct permissions
RUN chown -R nextjs:nodejs /app

USER nextjs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]

# Start the application
CMD ["node", "apps/web/server.js"]
126 changes: 20 additions & 106 deletions apps/web/src/components/organisms/chat-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,18 @@
"use client"

import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/src/components/ui/alert-dialog"
import { Button } from "@/src/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/src/components/ui/dropdown-menu"
import { Input } from "@/src/components/ui/input"
import { ScrollArea } from "@/src/components/ui/scroll-area"
import { Skeleton } from "@/src/components/ui/skeleton"
import useDeleteConversation from "@/src/hooks/chat/use-delete-conversation"
import useGetConversations from "@/src/hooks/chat/use-get-conversations"
import { cn } from "@/src/lib/utils"
import type { ConversationSummary } from "@/src/services/chat.services"
import { IconChevronLeft, IconDotsVertical, IconMessageCircle, IconSearch, IconTrash, IconX } from "@tabler/icons-react"
import { IconChevronLeft, IconMessageCircle, IconSearch, IconTrash, IconX } from "@tabler/icons-react"
import Link from "next/link"
import { usePathname, useRouter } from "next/navigation"
import { useMemo, useState } from "react"
import { toast } from "sonner"

// ---------------------------------------------------------------------------
// Date grouping
// ---------------------------------------------------------------------------

interface DateGroup {
label: string
items: ConversationSummary[]
Expand Down Expand Up @@ -69,10 +48,6 @@ const groupByDate = (conversations: ConversationSummary[]): DateGroup[] => {
return groups.filter((g) => g.items.length > 0)
}

// ---------------------------------------------------------------------------
// Skeleton
// ---------------------------------------------------------------------------

const ConversationSkeleton = () => (
<div className="flex flex-col gap-0.5 px-2 pt-4">
{[70, 50, 80, 55, 65, 45, 75].map((w, i) => (
Expand All @@ -84,35 +59,24 @@ const ConversationSkeleton = () => (
</div>
)

// ---------------------------------------------------------------------------
// Thread item
// ---------------------------------------------------------------------------

interface ThreadItemProps {
conv: ConversationSummary
isactive: boolean
onRequestDelete: (id: string) => void
onRequestDelete: (id: string) => Promise<unknown>
}

const ThreadItem = ({ conv, isactive, onRequestDelete }: ThreadItemProps) => (
<div
className={cn(
// overflow-hidden + w-full ensure the text truncation chain is complete
// and nothing leaks outside the item box
"group relative flex items-center w-full overflow-hidden rounded-md transition-colors",
isactive ? "bg-accent" : "hover:bg-accent/50",
)}
>
{/*
* min-w-0 on the link allows it to shrink below content width in flex.
* pr-7 (28px) reserves space for the 24px options button + 4px gap.
* Without pr-7 the button overlaps the text.
*/}
<Link
href={`/chat/${conv.id}`}
className={cn(
"flex-1 min-w-0 px-3 py-2 pr-7 rounded-md",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
"flex-1 min-w-0 px-3 py-2 pr-7 rounded-md max-w-96",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset ",
)}
>
<p
Expand All @@ -130,52 +94,32 @@ const ThreadItem = ({ conv, isactive, onRequestDelete }: ThreadItemProps) => (
)}
</Link>

{/* Options button — absolute so it doesn't affect link width */}
<div
className={cn(
"absolute right-1 top-1/2 -translate-y-1/2 shrink-0 transition-opacity",
"opacity-0 group-hover:opacity-100 group-focus-within:opacity-100",
isactive && "opacity-100",
)}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:text-foreground"
aria-label="Thread options"
>
<IconDotsVertical className="size-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-36">
<DropdownMenuItem
className="gap-2 text-destructive focus:text-destructive focus:bg-destructive/10"
onSelect={() => onRequestDelete(conv.id)}
>
<IconTrash className="size-3.5" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
size={"icon"}
variant={"outline"}
className="bg-transparent hover:bg-transparent border-none"
onClick={() => onRequestDelete(conv.id)}
>
<IconTrash color="red" />
</Button>
</div>
</div>
)

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

interface ChatSidebarProps {
onClose: () => void
}

const ChatSidebar = ({ onClose }: ChatSidebarProps) => {
const router = useRouter()
const pathname = usePathname()
const [search, setSearch] = useState("")
const [pendingdeleteid, setPendingdeleteid] = useState<string | null>(null)
const router = useRouter()

const activeconversationid = pathname.split("/chat/")[1] ?? null

Expand All @@ -195,28 +139,21 @@ const ChatSidebar = ({ onClose }: ChatSidebarProps) => {

const groups = useMemo(() => groupByDate(filtered), [filtered])

const handleDeleteConfirm = async () => {
if (!pendingdeleteid) return
const id = pendingdeleteid
setPendingdeleteid(null)
const handleDelete = async (id: string) => {
try {
await deleteConversation.mutateAsync(id)
toast.success("Conversation deleted")
if (activeconversationid === id) router.push("/chat")
} catch {
toast.error("Failed to delete conversation")
router.push("/chat")
} catch (error) {
toast.error("Failed to delete conversation", {
description: (error as Error).message,
})
}
}

return (
<>
{/*
* h-full fills the motion.div wrapper in sidebar.tsx (which itself
* fills the DesktopSidebar container). flex flex-col lets ScrollArea
* take the remaining height via flex-1.
*/}
<div className="flex flex-col h-full w-full bg-sidebar border-r">
{/* ── Header ── */}
<div className="flex items-center gap-1.5 px-4 pt-4 pb-3 shrink-0 border-b border-border/50">
<button
type="button"
Expand All @@ -229,7 +166,6 @@ const ChatSidebar = ({ onClose }: ChatSidebarProps) => {
<span className="text-sm font-semibold flex-1 text-foreground select-none">Threads</span>
</div>

{/* ── Search ── */}
<div className="px-3 py-2.5 shrink-0">
<div className="relative">
<IconSearch className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground pointer-events-none" />
Expand All @@ -252,7 +188,6 @@ const ChatSidebar = ({ onClose }: ChatSidebarProps) => {
</div>
</div>

{/* ── Thread list ── */}
<ScrollArea className="flex-1 min-h-0">
{loading ? (
<ConversationSkeleton />
Expand Down Expand Up @@ -286,7 +221,7 @@ const ChatSidebar = ({ onClose }: ChatSidebarProps) => {
key={conv.id}
conv={conv}
isactive={activeconversationid === conv.id}
onRequestDelete={setPendingdeleteid}
onRequestDelete={handleDelete}
/>
))}
</div>
Expand All @@ -296,27 +231,6 @@ const ChatSidebar = ({ onClose }: ChatSidebarProps) => {
)}
</ScrollArea>
</div>

{/* ── Delete confirmation ── */}
<AlertDialog open={pendingdeleteid !== null} onOpenChange={(open) => !open && setPendingdeleteid(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete thread?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete the conversation and all its messages. This cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteConfirm}
className="bg-destructive text-white hover:bg-destructive/90"
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export const LAST_LEGAL_UPDATE_DATE: string = format(new Date(2025, 10, 27), "MM

export const IS_DEV = process.env.NODE_ENV === "development"

export const GATEWAY_URL: string = process.env.NEXT_PUBLIC_GATEWAY_URL ?? "http://localhost:4000"
export const GATEWAY_URL: string =
process.env.GATEWAY_URL ?? process.env.NEXT_PUBLIC_GATEWAY_URL ?? "http://localhost:4000"

/**
* Root directory for the knowledge base filesystem.
Expand Down
29 changes: 29 additions & 0 deletions docker-compose.apps.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
services:
gateway:
env_file:
- .env.local
build:
context: .
dockerfile: packages/gateway/Dockerfile
ports:
- "30081:4000"
environment:
- NODE_ENV=production
restart: unless-stopped

web:
env_file:
- .env.local
build:
context: .
dockerfile: apps/web/Dockerfile
ports:
- "30080:3000"
environment:
- NODE_ENV=production
- NEXT_PUBLIC_BASE_URL=http://localhost:30080
- GATEWAY_URL=http://gateway:4000
- NEXT_PUBLIC_GATEWAY_URL=http://localhost:30081
depends_on:
- gateway
restart: unless-stopped
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
postgres:
image: pgvector/pgvector:pg16
image: pgvector/pgvector:pg18
ports:
- "5432:5432"
environment:
Expand Down
7 changes: 7 additions & 0 deletions infra/openshell/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ COPY packages/sandbox-server/package.json packages/sandbox-server/
COPY packages/agents/package.json packages/agents/
COPY packages/logger/package.json packages/logger/
COPY packages/memory/package.json packages/memory/
COPY packages/db/package.json packages/db/
COPY packages/integrations/package.json packages/integrations/
COPY packages/skills/reports/package.json packages/skills/reports/

# Install dependencies
RUN pnpm install --frozen-lockfile
Expand All @@ -57,6 +60,10 @@ RUN pnpm install --frozen-lockfile
COPY packages/ packages/

RUN pnpm --filter @openzosma/logger run build
RUN pnpm --filter @openzosma/memory run build
RUN pnpm --filter @openzosma/db run build
RUN pnpm --filter @openzosma/integrations run build
RUN pnpm --filter @openzosma/skill-reports run build
RUN pnpm --filter @openzosma/agents run build
RUN pnpm --filter @openzosma/sandbox-server run build

Expand Down
Loading
Loading