mirror of
https://github.com/simstudioai/sim.git
synced 2026-03-15 03:00:33 -04:00
Task management
This commit is contained in:
@@ -103,6 +103,7 @@ export async function POST(req: NextRequest) {
|
||||
userId: authenticatedUserId,
|
||||
workspaceId,
|
||||
model: 'claude-opus-4-5',
|
||||
type: 'mothership',
|
||||
})
|
||||
currentChat = chatResult.chat
|
||||
actualChatId = chatResult.chatId || chatId
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db } from '@sim/db'
|
||||
import { copilotChats } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, desc, eq, isNull } from 'drizzle-orm'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
@@ -40,7 +40,7 @@ export async function GET(request: NextRequest) {
|
||||
and(
|
||||
eq(copilotChats.userId, userId),
|
||||
eq(copilotChats.workspaceId, workspaceId),
|
||||
isNull(copilotChats.workflowId)
|
||||
eq(copilotChats.type, 'mothership')
|
||||
)
|
||||
)
|
||||
.orderBy(desc(copilotChats.updatedAt))
|
||||
@@ -75,6 +75,7 @@ export async function POST(request: NextRequest) {
|
||||
.values({
|
||||
userId,
|
||||
workspaceId,
|
||||
type: 'mothership',
|
||||
title: null,
|
||||
model: 'claude-opus-4-5',
|
||||
messages: [],
|
||||
|
||||
@@ -16,18 +16,15 @@ export function Home({ chatId, streamId }: HomeProps = {}) {
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>()
|
||||
const router = useRouter()
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const { messages, isSending, sendMessage, stopGeneration, chatBottomRef } = useChat(
|
||||
workspaceId,
|
||||
chatId,
|
||||
streamId
|
||||
)
|
||||
const { messages, isSending, currentChatId, sendMessage, stopGeneration, chatBottomRef } =
|
||||
useChat(workspaceId, chatId, streamId)
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
const trimmed = inputValue.trim()
|
||||
if (!trimmed) return
|
||||
setInputValue('')
|
||||
|
||||
if (chatId) {
|
||||
if (chatId || currentChatId) {
|
||||
sendMessage(trimmed)
|
||||
return
|
||||
}
|
||||
@@ -54,7 +51,7 @@ export function Home({ chatId, streamId }: HomeProps = {}) {
|
||||
} catch {
|
||||
setInputValue(trimmed)
|
||||
}
|
||||
}, [inputValue, chatId, sendMessage, workspaceId, router])
|
||||
}, [inputValue, chatId, currentChatId, sendMessage, workspaceId, router])
|
||||
|
||||
const hasMessages = messages.length > 0
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface UseChatReturn {
|
||||
messages: ChatMessage[]
|
||||
isSending: boolean
|
||||
error: string | null
|
||||
currentChatId: string | undefined
|
||||
sendMessage: (message: string) => Promise<void>
|
||||
stopGeneration: () => void
|
||||
chatBottomRef: React.RefObject<HTMLDivElement | null>
|
||||
@@ -384,5 +385,13 @@ export function useChat(
|
||||
setIsSending(false)
|
||||
}, [])
|
||||
|
||||
return { messages, isSending, error, sendMessage, stopGeneration, chatBottomRef }
|
||||
return {
|
||||
messages,
|
||||
isSending,
|
||||
error,
|
||||
currentChatId: chatIdRef.current,
|
||||
sendMessage,
|
||||
stopGeneration,
|
||||
chatBottomRef,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,36 +3,15 @@
|
||||
import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
|
||||
|
||||
interface NavItemContextMenuProps {
|
||||
/**
|
||||
* Whether the context menu is open
|
||||
*/
|
||||
isOpen: boolean
|
||||
/**
|
||||
* Position of the context menu
|
||||
*/
|
||||
position: { x: number; y: number }
|
||||
/**
|
||||
* Ref for the menu element
|
||||
*/
|
||||
menuRef: React.RefObject<HTMLDivElement | null>
|
||||
/**
|
||||
* Callback when menu should close
|
||||
*/
|
||||
onClose: () => void
|
||||
/**
|
||||
* Callback when open in new tab is clicked
|
||||
*/
|
||||
onOpenInNewTab: () => void
|
||||
/**
|
||||
* Callback when copy link is clicked
|
||||
*/
|
||||
onCopyLink: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Context menu component for sidebar navigation items.
|
||||
* Displays navigation-appropriate options (open in new tab, copy link) in a popover at the right-click position.
|
||||
*/
|
||||
export function NavItemContextMenu({
|
||||
isOpen,
|
||||
position,
|
||||
@@ -40,6 +19,7 @@ export function NavItemContextMenu({
|
||||
onClose,
|
||||
onOpenInNewTab,
|
||||
onCopyLink,
|
||||
onDelete,
|
||||
}: NavItemContextMenuProps) {
|
||||
return (
|
||||
<Popover
|
||||
@@ -75,6 +55,17 @@ export function NavItemContextMenu({
|
||||
>
|
||||
Copy link
|
||||
</PopoverItem>
|
||||
{onDelete && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onDelete()
|
||||
onClose()
|
||||
}}
|
||||
className='text-[var(--color-error)]'
|
||||
>
|
||||
Delete
|
||||
</PopoverItem>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
useImportWorkflow,
|
||||
useImportWorkspace,
|
||||
} from '@/app/workspace/[workspaceId]/w/hooks'
|
||||
import { useTasks } from '@/hooks/queries/tasks'
|
||||
import { useDeleteTask, useTasks } from '@/hooks/queries/tasks'
|
||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||
import { SIDEBAR_WIDTH } from '@/stores/constants'
|
||||
import { useFolderStore } from '@/stores/folders/store'
|
||||
@@ -183,6 +183,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
})
|
||||
|
||||
const [activeNavItemHref, setActiveNavItemHref] = useState<string | null>(null)
|
||||
const [activeTaskId, setActiveTaskId] = useState<string | null>(null)
|
||||
const {
|
||||
isOpen: isNavContextMenuOpen,
|
||||
position: navContextMenuPosition,
|
||||
@@ -191,9 +192,21 @@ export const Sidebar = memo(function Sidebar() {
|
||||
closeMenu: closeNavContextMenu,
|
||||
} = useContextMenu()
|
||||
|
||||
const deleteTaskMutation = useDeleteTask(workspaceId)
|
||||
|
||||
const handleNavItemContextMenu = useCallback(
|
||||
(e: React.MouseEvent, href: string) => {
|
||||
setActiveNavItemHref(href)
|
||||
setActiveTaskId(null)
|
||||
handleNavContextMenuBase(e)
|
||||
},
|
||||
[handleNavContextMenuBase]
|
||||
)
|
||||
|
||||
const handleTaskContextMenu = useCallback(
|
||||
(e: React.MouseEvent, href: string, taskId: string) => {
|
||||
setActiveNavItemHref(href)
|
||||
setActiveTaskId(taskId)
|
||||
handleNavContextMenuBase(e)
|
||||
},
|
||||
[handleNavContextMenuBase]
|
||||
@@ -202,6 +215,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
const handleNavContextMenuClose = useCallback(() => {
|
||||
closeNavContextMenu()
|
||||
setActiveNavItemHref(null)
|
||||
setActiveTaskId(null)
|
||||
}, [closeNavContextMenu])
|
||||
|
||||
const handleNavOpenInNewTab = useCallback(() => {
|
||||
@@ -221,6 +235,18 @@ export const Sidebar = memo(function Sidebar() {
|
||||
}
|
||||
}, [activeNavItemHref])
|
||||
|
||||
const handleDeleteTask = useCallback(() => {
|
||||
if (!activeTaskId) return
|
||||
const isViewingDeletedTask = pathname === `/workspace/${workspaceId}/task/${activeTaskId}`
|
||||
deleteTaskMutation.mutate(activeTaskId, {
|
||||
onSuccess: () => {
|
||||
if (isViewingDeletedTask) {
|
||||
router.push(`/workspace/${workspaceId}/home`)
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [activeTaskId, pathname, workspaceId, deleteTaskMutation, router])
|
||||
|
||||
const { handleDuplicateWorkspace: duplicateWorkspace } = useDuplicateWorkspace({
|
||||
workspaceId,
|
||||
})
|
||||
@@ -687,7 +713,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
key={task.id}
|
||||
href={task.href}
|
||||
className={`mx-[2px] flex h-[28px] items-center gap-[8px] rounded-[8px] px-[8px] text-[14px] hover:bg-[var(--surface-6)] dark:hover:bg-[var(--surface-5)] ${active ? 'bg-[var(--surface-6)] dark:bg-[var(--surface-5)]' : ''}`}
|
||||
onContextMenu={(e) => handleNavItemContextMenu(e, task.href)}
|
||||
onContextMenu={(e) => handleTaskContextMenu(e, task.href, task.id)}
|
||||
>
|
||||
<Blimp className={`h-[14px] w-[14px] flex-shrink-0 ${textColor}`} />
|
||||
<div className={`min-w-0 truncate font-medium ${textColor}`}>{task.name}</div>
|
||||
@@ -813,6 +839,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
onClose={handleNavContextMenuClose}
|
||||
onOpenInNewTab={handleNavOpenInNewTab}
|
||||
onCopyLink={handleNavCopyLink}
|
||||
onDelete={activeTaskId ? handleDeleteTask : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
export interface TaskMetadata {
|
||||
id: string
|
||||
@@ -105,3 +105,27 @@ export function useChatHistory(chatId: string | undefined) {
|
||||
staleTime: 30 * 1000,
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteTask(chatId: string): Promise<void> {
|
||||
const response = await fetch('/api/copilot/chat/delete', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ chatId }),
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete task')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a mothership chat task and invalidates the task list.
|
||||
*/
|
||||
export function useDeleteTask(workspaceId?: string) {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: deleteTask,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,8 +23,9 @@ export async function resolveOrCreateChat(params: {
|
||||
workflowId?: string
|
||||
workspaceId?: string
|
||||
model: string
|
||||
type?: 'mothership' | 'copilot'
|
||||
}): Promise<ChatLoadResult> {
|
||||
const { chatId, userId, workflowId, workspaceId, model } = params
|
||||
const { chatId, userId, workflowId, workspaceId, model, type } = params
|
||||
|
||||
if (chatId) {
|
||||
const [chat] = await db
|
||||
@@ -47,6 +48,7 @@ export async function resolveOrCreateChat(params: {
|
||||
userId,
|
||||
...(workflowId ? { workflowId } : {}),
|
||||
...(workspaceId ? { workspaceId } : {}),
|
||||
type: type ?? 'copilot',
|
||||
title: null,
|
||||
model,
|
||||
messages: [],
|
||||
|
||||
7
packages/db/migrations/0163_chat_type_column.sql
Normal file
7
packages/db/migrations/0163_chat_type_column.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."chat_type" AS ENUM('mothership', 'copilot');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "copilot_chats" ADD COLUMN "type" "public"."chat_type" DEFAULT 'copilot' NOT NULL;
|
||||
2
packages/db/migrations/0163_pink_gambit.sql
Normal file
2
packages/db/migrations/0163_pink_gambit.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
CREATE TYPE "public"."chat_type" AS ENUM('mothership', 'copilot');--> statement-breakpoint
|
||||
ALTER TABLE "copilot_chats" ADD COLUMN "type" "chat_type" DEFAULT 'copilot' NOT NULL;
|
||||
13107
packages/db/migrations/meta/0163_snapshot.json
Normal file
13107
packages/db/migrations/meta/0163_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1135,6 +1135,13 @@
|
||||
"when": 1772482049606,
|
||||
"tag": "0162_early_bloodscream",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 163,
|
||||
"version": "7",
|
||||
"when": 1772567614060,
|
||||
"tag": "0163_pink_gambit",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1489,6 +1489,8 @@ export const docsEmbeddings = pgTable(
|
||||
})
|
||||
)
|
||||
|
||||
export const chatTypeEnum = pgEnum('chat_type', ['mothership', 'copilot'])
|
||||
|
||||
export const copilotChats = pgTable(
|
||||
'copilot_chats',
|
||||
{
|
||||
@@ -1498,13 +1500,14 @@ export const copilotChats = pgTable(
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
workflowId: text('workflow_id').references(() => workflow.id, { onDelete: 'cascade' }),
|
||||
workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }),
|
||||
type: chatTypeEnum('type').notNull().default('copilot'),
|
||||
title: text('title'),
|
||||
messages: jsonb('messages').notNull().default('[]'),
|
||||
model: text('model').notNull().default('claude-3-7-sonnet-latest'),
|
||||
conversationId: text('conversation_id'),
|
||||
previewYaml: text('preview_yaml'), // YAML content for pending workflow preview
|
||||
planArtifact: text('plan_artifact'), // Plan/design document artifact for the chat
|
||||
config: jsonb('config'), // JSON config storing model and mode settings { model, mode }
|
||||
previewYaml: text('preview_yaml'),
|
||||
planArtifact: text('plan_artifact'),
|
||||
config: jsonb('config'),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user