Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
79633d3
feat(media): add tool mode state and actions to workspace media hook
derianrddev Feb 19, 2026
191d323
feat(media): add MediaToolbar component for canvas drawing tools
derianrddev Feb 19, 2026
8663d6e
refactor(media): rewrite MediaCanvas with observer-based responsive s…
derianrddev Feb 19, 2026
1edb6b4
refactor(media): replace custom scroll container with ScrollArea in M…
derianrddev Feb 19, 2026
220a380
feat(media): update AI actions for media tab with edit/expand/redesig…
derianrddev Feb 19, 2026
7230f9d
feat(canvas): add canvas layer types and coordinate utilities
derianrddev Feb 20, 2026
a8f64ab
feat(canvas): add CanvasRenderSizeContext for pixel-accurate layer po…
derianrddev Feb 20, 2026
9354966
feat(canvas): add canvas layer state and actions to workspace media hook
derianrddev Feb 20, 2026
c28639f
feat(canvas): add TextBoxElement and CanvasLayersContainer components
derianrddev Feb 20, 2026
ac3b1df
feat(canvas): integrate layer rendering and render size context into …
derianrddev Feb 20, 2026
5cafab0
feat(canvas): add SelectionOverlay and FloatingTextToolbar for layer …
derianrddev Feb 23, 2026
2f9c13e
feat(canvas): add inline editing to TextBoxElement with editingLayerI…
derianrddev Feb 23, 2026
5c07603
feat(canvas): add drag-to-reposition to TextBoxElement
derianrddev Feb 23, 2026
9a9bd23
feat(canvas): replace FloatingTextToolbar stub with full text style c…
derianrddev Feb 23, 2026
564e75a
feat(canvas): add global keyboard shortcuts for layer deletion and te…
derianrddev Feb 24, 2026
2f725ed
feat(canvas): add resize handles to TextBoxElement
derianrddev Feb 24, 2026
25080c1
feat(canvas): implement flatten-to-image with undo snapshot
derianrddev Feb 25, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,25 @@ import {
import { useThread } from '@/lib/hooks/use-thread'
import { useWorkspace } from '@/lib/hooks/use-workspace'
import { useWorkspaceChat } from '@/lib/hooks/use-workspace-chat'
import { useWorkspaceMedia } from '@/lib/hooks/use-workspace-media'
import { parseMarkdownSections } from '@/lib/markdown-utils'
import { useDocumentContent } from '@/lib/queries'
import { cn } from '@/lib/utils'
import type { WorkspaceTabType } from '@/types/thread.types'
import {
ArrowLeftRight,
CheckSquareIcon,
ChevronLeft,
Edit3Icon,
Eraser,
Expand,
ListEndIcon,
type LucideIcon,
PenTool,
Pencil,
RotateCcwIcon,
Sparkles,
TextIcon,
Wand2,
} from 'lucide-react'
import { Fragment, useCallback, useMemo, useState } from 'react'
import { Fragment, useMemo, useState } from 'react'

interface AIAction {
id: string
Expand Down Expand Up @@ -90,6 +92,7 @@ export function AIActionsDropdown({
} = useWorkspace()
const { activeWorkspaceSection } = useWorkspaceChat()
const { activeThread } = useThread()
const { isToolMode } = useWorkspaceMedia()
const [isOpen, setIsOpen] = useState(false)
const { data: documentData } = useDocumentContent(
activeProject,
Expand All @@ -102,52 +105,66 @@ export function AIActionsDropdown({
// Get actions based on active tab
const actions = useMemo(() => getActionsForTab(activeTab), [activeTab])

// Don't show if workspace is not active or no project/document selected
if (!isWorkspaceActive || !activeProject || !activeDocument) {
return null
}
// Documents require project+document, media does not
const isDocumentTab = activeTab === 'documents'
if (!isWorkspaceActive) return null
if (isDocumentTab && (!activeProject || !activeDocument)) return null

// Don't show if no actions available for this tab
if (actions.length === 0) {
return null
}

const handleActionClick = (e: React.MouseEvent, actionId: string) => {
const isDocumentsPayload = actionId.startsWith('document-')
const isMediaPayload = actionId.startsWith('media-')
const activeDocumentSection =
(
documentData?.sections ||
parseMarkdownSections(documentData?.content || '')
).find((section) => section.id === activeWorkspaceSection) || null
const actionPayloads = {
...(activeDocumentSection
? { documentSection: activeDocumentSection }
: {}),
error: {
document:
isDocumentsPayload &&
!activeDocumentSection &&
'Please select a document and a section before to trigger this action.',
media:
isMediaPayload &&
'Please add or create an image before to trigger this action.',
},
const handleActionClick = (actionId: string) => {
// Tool Mode is a pure toggle — always fires, does not track in activeAction
if (actionId === 'media-tool-mode') {
onAction(actionId)
setIsOpen(false)
return
}
const actionToTrigger = !actionPayloads.documentSection
? 'cancel-action'
: actionId
const shouldTriggerAction = actionToTrigger !== activeAction
const isDocumentAction = actionId.startsWith('document-')

setActiveAction(
shouldTriggerAction && actionToTrigger !== 'cancel-action'
? actionToTrigger
: '',
)
setIsOpen(false)
if (isDocumentAction) {
const activeDocumentSection =
(
documentData?.sections ||
parseMarkdownSections(documentData?.content || '')
).find((section) => section.id === activeWorkspaceSection) || null
const actionPayloads = {
...(activeDocumentSection
? { documentSection: activeDocumentSection }
: {}),
error: {
document:
!activeDocumentSection &&
'Please select a document and a section before to trigger this action.',
media: '',
},
}
const actionToTrigger = !actionPayloads.documentSection
? 'cancel-action'
: actionId
const shouldTriggerAction = actionToTrigger !== activeAction

setActiveAction(
shouldTriggerAction && actionToTrigger !== 'cancel-action'
? actionToTrigger
: '',
)
setIsOpen(false)

if (shouldTriggerAction) {
onAction(actionToTrigger, actionPayloads)
}
return
}

// Media and other actions — no section requirement
const shouldTriggerAction = actionId !== activeAction
setActiveAction(shouldTriggerAction ? actionId : '')
setIsOpen(false)
if (shouldTriggerAction) {
onAction(actionToTrigger, actionPayloads)
onAction(actionId)
}
}

Expand Down Expand Up @@ -203,12 +220,15 @@ export function AIActionsDropdown({
return (
<Fragment key={action.id}>
<DropdownMenuItem
onClick={(e) => handleActionClick(e, action.id)}
onClick={() => handleActionClick(action.id)}
disabled={disabled}
className={cn(
'cursor-pointer hover:bg-chat hover:border-chat hover:text-chat-foreground',
activeAction === action.id &&
'bg-chat border-chat text-chat-foreground',
action.id === 'media-tool-mode' &&
isToolMode &&
'bg-chat border-chat text-chat-foreground',
)}
title={action.description}
>
Expand Down Expand Up @@ -269,24 +289,29 @@ function getActionsForTab(activeTab: WorkspaceTabType): AIAction[] {
case 'media':
return [
{
id: 'erase-content',
label: 'Erase',
icon: Eraser,
description: 'Erase content in selection',
requiresSelection: true,
id: 'media-edit',
label: 'Edit',
icon: Pencil,
description: 'Edit image with AI',
},
{
id: 'replace-content',
label: 'Replace',
icon: ArrowLeftRight,
description: 'Replace content in selection',
requiresSelection: true,
id: 'media-expand',
label: 'Expand',
icon: Expand,
iconClassName: 'scale-x-[-1]',
description: 'Expand image (coming soon)',
},
{
id: 'media-redesign',
label: 'Redesign',
icon: Wand2,
description: 'Redesign image (coming soon)',
},
{
id: 'add-text',
label: 'Add Text',
icon: TextIcon,
description: 'Insert text into image',
id: 'media-tool-mode',
label: 'Tool Mode',
icon: PenTool,
description: 'Toggle manual drawing tools',
},
]

Expand Down
54 changes: 37 additions & 17 deletions apps/pro-web/components/routes/chat/prompt-form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export function PromptForm({
} = useWorkspace()
const {
activeGenerationScope,
generatedImage,
getGenerationInstance,
getSelectedColors,
handleMediaPromptSubmit,
Expand All @@ -157,7 +158,9 @@ export function PromptForm({
selectedFonts,
selectedPalette,
selectedSymbolStyle,
selectedTemplate,
selectedVibes,
toggleToolMode,
wordmarkCasing,
wordmarkFontFamily,
wordmarkFontWeight,
Expand Down Expand Up @@ -416,15 +419,17 @@ Symbol to create: `
// Handle AI action requests from dropdown menu
const handleAIAction = useCallback(
async (actionId: string, payload?: Record<string, unknown>) => {
if (!isWorkspaceActive || !activeProject || !activeDocument) {
const isMediaAction = actionId.startsWith('media-')
if (!isWorkspaceActive) return
if (!isMediaAction && (!activeProject || !activeDocument)) {
console.warn('AI action attempted without active workspace')
return
}

let prompt = ''

// Payload Destructuring
const { documentSection, error } = payload as {
const { documentSection, error } = (payload ?? {}) as {
documentSection: MarkdownSection | null
error: {
document: string
Expand Down Expand Up @@ -456,30 +461,37 @@ Symbol to create: `
break
}

// TODO: Media Actions
case 'media-erase': {
prompt = 'Erase the main element inside of the red oval'
console.log('Erase content action - to be implemented')
// ? Media Actions
case 'media-edit': {
if (!generatedImage && !selectedTemplate) {
customSonner({
type: 'info',
text: 'Please select a template or generate an image first.',
})
return
}
prompt = 'Edit: '
break
}
case 'media-replace': {
prompt = 'Replace the main element inside of the red oval with '
console.log('Replace content action - to be implemented')
break
case 'media-expand': {
customSonner({ type: 'info', text: 'Coming soon!' })
return
}
case 'media-add-text': {
prompt =
'Integrate the over text into the image. Match style and text'
console.log('Add text action - to be implemented')
break
case 'media-redesign': {
customSonner({ type: 'info', text: 'Coming soon!' })
return
}
case 'media-tool-mode': {
toggleToolMode()
return
}

// ? General Actions
case 'cancel-action': {
console.warn('Cancelling action...')
customSonner({
type: 'error',
text: error.document || error.media,
text: error?.document || error?.media,
})
break
}
Expand All @@ -500,7 +512,15 @@ Symbol to create: `
}
}, 50)
},
[isWorkspaceActive, activeProject, activeDocument, setInput],
[
isWorkspaceActive,
activeProject,
activeDocument,
setInput,
generatedImage,
selectedTemplate,
toggleToolMode,
],
)

const { customSonner } = useSonner()
Expand Down
Loading