Skip to content

Commit d806c47

Browse files
authored
Merge pull request #6586 from menloresearch/feat/thread-project-org
feat: thread organization folder
2 parents 978565e + d690e0f commit d806c47

28 files changed

+1871
-287
lines changed

web-app/src/components/ui/__tests__/dialog.test.tsx

Lines changed: 114 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { render, screen, fireEvent } from '@testing-library/react'
22
import { describe, it, expect, vi } from 'vitest'
33
import userEvent from '@testing-library/user-event'
44
import React from 'react'
5+
import '@testing-library/jest-dom'
56
import {
67
Dialog,
78
DialogTrigger,
@@ -117,7 +118,7 @@ describe('Dialog Components', () => {
117118

118119
it('applies proper classes to dialog content', async () => {
119120
const user = userEvent.setup()
120-
121+
121122
render(
122123
<Dialog>
123124
<DialogTrigger>Open Dialog</DialogTrigger>
@@ -128,27 +129,38 @@ describe('Dialog Components', () => {
128129
</DialogContent>
129130
</Dialog>
130131
)
131-
132+
132133
await user.click(screen.getByText('Open Dialog'))
133-
134+
134135
const dialogContent = screen.getByRole('dialog')
135136
expect(dialogContent).toHaveClass(
136137
'bg-main-view',
138+
'max-h-[calc(100%-80px)]',
139+
'overflow-auto',
140+
'border-main-view-fg/10',
141+
'text-main-view-fg',
137142
'fixed',
138143
'top-[50%]',
139144
'left-[50%]',
140-
'z-50',
145+
'z-[90]',
146+
'grid',
147+
'w-full',
148+
'max-w-[calc(100%-2rem)]',
141149
'translate-x-[-50%]',
142150
'translate-y-[-50%]',
143-
'border',
151+
'gap-4',
144152
'rounded-lg',
145-
'shadow-lg'
153+
'border',
154+
'p-6',
155+
'shadow-lg',
156+
'duration-200',
157+
'sm:max-w-lg'
146158
)
147159
})
148160

149161
it('applies proper classes to dialog header', async () => {
150162
const user = userEvent.setup()
151-
163+
152164
render(
153165
<Dialog>
154166
<DialogTrigger>Open Dialog</DialogTrigger>
@@ -159,11 +171,11 @@ describe('Dialog Components', () => {
159171
</DialogContent>
160172
</Dialog>
161173
)
162-
174+
163175
await user.click(screen.getByText('Open Dialog'))
164-
176+
165177
const dialogHeader = screen.getByText('Dialog Title').closest('div')
166-
expect(dialogHeader).toHaveClass('flex', 'flex-col', 'gap-2', 'text-center')
178+
expect(dialogHeader).toHaveClass('flex', 'flex-col', 'gap-2', 'text-center', 'sm:text-left')
167179
})
168180

169181
it('applies proper classes to dialog title', async () => {
@@ -299,7 +311,7 @@ describe('Dialog Components', () => {
299311
it('supports onOpenChange callback', async () => {
300312
const onOpenChange = vi.fn()
301313
const user = userEvent.setup()
302-
314+
303315
render(
304316
<Dialog onOpenChange={onOpenChange}>
305317
<DialogTrigger>Open Dialog</DialogTrigger>
@@ -310,9 +322,98 @@ describe('Dialog Components', () => {
310322
</DialogContent>
311323
</Dialog>
312324
)
313-
325+
314326
await user.click(screen.getByText('Open Dialog'))
315-
327+
316328
expect(onOpenChange).toHaveBeenCalledWith(true)
317329
})
330+
331+
it('can hide close button when showCloseButton is false', async () => {
332+
const user = userEvent.setup()
333+
334+
render(
335+
<Dialog>
336+
<DialogTrigger>Open Dialog</DialogTrigger>
337+
<DialogContent showCloseButton={false}>
338+
<DialogHeader>
339+
<DialogTitle>Dialog Title</DialogTitle>
340+
</DialogHeader>
341+
</DialogContent>
342+
</Dialog>
343+
)
344+
345+
await user.click(screen.getByText('Open Dialog'))
346+
347+
expect(screen.queryByRole('button', { name: /close/i })).not.toBeInTheDocument()
348+
})
349+
350+
it('shows close button by default', async () => {
351+
const user = userEvent.setup()
352+
353+
render(
354+
<Dialog>
355+
<DialogTrigger>Open Dialog</DialogTrigger>
356+
<DialogContent>
357+
<DialogHeader>
358+
<DialogTitle>Dialog Title</DialogTitle>
359+
</DialogHeader>
360+
</DialogContent>
361+
</Dialog>
362+
)
363+
364+
await user.click(screen.getByText('Open Dialog'))
365+
366+
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument()
367+
})
368+
369+
it('accepts aria-describedby prop', async () => {
370+
const user = userEvent.setup()
371+
372+
render(
373+
<Dialog>
374+
<DialogTrigger>Open Dialog</DialogTrigger>
375+
<DialogContent aria-describedby="custom-description">
376+
<DialogHeader>
377+
<DialogTitle>Dialog Title</DialogTitle>
378+
</DialogHeader>
379+
<p id="custom-description">Custom description text</p>
380+
</DialogContent>
381+
</Dialog>
382+
)
383+
384+
await user.click(screen.getByText('Open Dialog'))
385+
386+
const dialogContent = screen.getByRole('dialog')
387+
expect(dialogContent).toHaveAttribute('aria-describedby', 'custom-description')
388+
})
389+
390+
it('applies data-slot attributes to components', async () => {
391+
const user = userEvent.setup()
392+
393+
render(
394+
<Dialog>
395+
<DialogTrigger>Open Dialog</DialogTrigger>
396+
<DialogContent>
397+
<DialogHeader>
398+
<DialogTitle>Dialog Title</DialogTitle>
399+
<DialogDescription>Dialog description</DialogDescription>
400+
</DialogHeader>
401+
<div>Dialog body content</div>
402+
<DialogFooter>
403+
<button>Footer button</button>
404+
</DialogFooter>
405+
</DialogContent>
406+
</Dialog>
407+
)
408+
409+
expect(screen.getByText('Open Dialog')).toHaveAttribute('data-slot', 'dialog-trigger')
410+
411+
await user.click(screen.getByText('Open Dialog'))
412+
413+
expect(screen.getByRole('dialog')).toHaveAttribute('data-slot', 'dialog-content')
414+
expect(screen.getByText('Dialog Title').closest('div')).toHaveAttribute('data-slot', 'dialog-header')
415+
expect(screen.getByText('Dialog Title')).toHaveAttribute('data-slot', 'dialog-title')
416+
expect(screen.getByText('Dialog description')).toHaveAttribute('data-slot', 'dialog-description')
417+
expect(screen.getByText('Footer button').closest('div')).toHaveAttribute('data-slot', 'dialog-footer')
418+
})
318419
})

web-app/src/components/ui/dialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function DialogOverlay({
3737
<DialogPrimitive.Overlay
3838
data-slot="dialog-overlay"
3939
className={cn(
40-
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-main-view/80 backdrop-blur-sm',
40+
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[80] bg-main-view/80 backdrop-blur-sm',
4141
className
4242
)}
4343
{...props}
@@ -67,7 +67,7 @@ function DialogContent({
6767
data-slot="dialog-content"
6868
aria-describedby={ariaDescribedBy}
6969
className={cn(
70-
'bg-main-view max-h-[calc(100%-80px)] overflow-auto border-main-view-fg/10 text-main-view-fg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
70+
'bg-main-view max-h-[calc(100%-80px)] overflow-auto border-main-view-fg/10 text-main-view-fg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[90] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
7171
className
7272
)}
7373
{...props}

web-app/src/constants/localStorage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ export const localStorageKey = {
2121
lastUsedAssistant: 'last-used-assistant',
2222
favoriteModels: 'favorite-models',
2323
setupCompleted: 'setup-completed',
24+
threadManagement: 'thread-management',
2425
}

web-app/src/constants/routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ export const route = {
33
home: '/',
44
appLogs: '/logs',
55
assistant: '/assistant',
6+
project: '/project',
7+
projectDetail: '/project/$projectId',
68
settings: {
79
index: '/settings',
810
model_providers: '/settings/providers',

web-app/src/containers/ChatInput.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import TextareaAutosize from 'react-textarea-autosize'
44
import { cn } from '@/lib/utils'
55
import { usePrompt } from '@/hooks/usePrompt'
66
import { useThreads } from '@/hooks/useThreads'
7+
import { useThreadManagement } from '@/hooks/useThreadManagement'
78
import { useCallback, useEffect, useRef, useState } from 'react'
89
import { Button } from '@/components/ui/button'
910
import {
@@ -43,9 +44,15 @@ type ChatInputProps = {
4344
showSpeedToken?: boolean
4445
model?: ThreadModel
4546
initialMessage?: boolean
47+
projectId?: string
4648
}
4749

48-
const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
50+
const ChatInput = ({
51+
model,
52+
className,
53+
initialMessage,
54+
projectId,
55+
}: ChatInputProps) => {
4956
const textareaRef = useRef<HTMLTextAreaElement>(null)
5057
const [isFocused, setIsFocused] = useState(false)
5158
const [rows, setRows] = useState(1)
@@ -58,6 +65,8 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
5865
const prompt = usePrompt((state) => state.prompt)
5966
const setPrompt = usePrompt((state) => state.setPrompt)
6067
const currentThreadId = useThreads((state) => state.currentThreadId)
68+
const updateThread = useThreads((state) => state.updateThread)
69+
const { getFolderById } = useThreadManagement()
6170
const { t } = useTranslation()
6271
const spellCheckChatInput = useGeneralSetting(
6372
(state) => state.spellCheckChatInput
@@ -177,6 +186,28 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
177186
uploadedFiles.length > 0 ? uploadedFiles : undefined
178187
)
179188
setUploadedFiles([])
189+
190+
// Handle project assignment for new threads
191+
if (projectId && !currentThreadId) {
192+
const project = getFolderById(projectId)
193+
if (project) {
194+
// Use setTimeout to ensure the thread is created first
195+
setTimeout(() => {
196+
const newCurrentThreadId = useThreads.getState().currentThreadId
197+
if (newCurrentThreadId) {
198+
updateThread(newCurrentThreadId, {
199+
metadata: {
200+
project: {
201+
id: project.id,
202+
name: project.name,
203+
updated_at: project.updated_at,
204+
},
205+
},
206+
})
207+
}
208+
}, 100)
209+
}
210+
}
180211
}
181212

182213
useEffect(() => {

0 commit comments

Comments
 (0)