-
Notifications
You must be signed in to change notification settings - Fork 273
feat(amazonq): add image context support #5846
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
92b30dc
79f86f6
9d0fd61
4621012
eb596bd
2594dbe
42016ee
3df8ef9
410c68e
ac84d65
714a4a1
dfce2b1
0c18a65
024d33b
388b428
6218560
c303aac
b95ed96
351b6a1
566b82c
8e9fa3a
1834f27
fe318e4
cfe76d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"type" : "feature", | ||
"description" : "Add image context support" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
|
||
package software.aws.toolkits.jetbrains.services.amazonq.toolwindow | ||
|
||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper | ||
import com.intellij.idea.AppMode | ||
import com.intellij.openapi.Disposable | ||
import com.intellij.openapi.components.service | ||
|
@@ -20,6 +21,8 @@ import kotlinx.coroutines.CoroutineScope | |
import kotlinx.coroutines.flow.first | ||
import kotlinx.coroutines.launch | ||
import kotlinx.coroutines.withContext | ||
import software.aws.toolkits.core.utils.error | ||
import software.aws.toolkits.core.utils.getLogger | ||
import software.aws.toolkits.jetbrains.core.coroutines.EDT | ||
import software.aws.toolkits.jetbrains.isDeveloperMode | ||
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext | ||
|
@@ -44,7 +47,12 @@ import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable | |
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable | ||
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable | ||
import software.aws.toolkits.resources.message | ||
import java.awt.datatransfer.DataFlavor | ||
import java.awt.dnd.DropTarget | ||
import java.awt.dnd.DropTargetDropEvent | ||
import java.io.File | ||
import java.util.concurrent.CompletableFuture | ||
import javax.imageio.ImageIO.read | ||
import javax.swing.JButton | ||
|
||
class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Disposable { | ||
|
@@ -122,12 +130,76 @@ class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Di | |
|
||
withContext(EDT) { | ||
browser.complete( | ||
Browser(this@AmazonQPanel, webUri, project).also { | ||
wrapper.setContent(it.component()) | ||
Browser(this@AmazonQPanel, webUri, project).also { browserInstance -> | ||
wrapper.setContent(browserInstance.component()) | ||
|
||
// Add DropTarget to the browser component | ||
// JCEF does not propagate OS-level dragenter, dragOver and drop into DOM. | ||
// As an alternative, enabling the native drag in JCEF, | ||
// and let the native handling the drop event, and update the UI through JS bridge. | ||
val dropTarget = object : DropTarget() { | ||
override fun drop(dtde: DropTargetDropEvent) { | ||
try { | ||
dtde.acceptDrop(dtde.dropAction) | ||
val transferable = dtde.transferable | ||
if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { | ||
val fileList = transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*> | ||
|
||
val errorMessages = mutableListOf<String>() | ||
val validImages = mutableListOf<File>() | ||
val allowedTypes = setOf("jpg", "jpeg", "png", "gif", "webp") | ||
val maxFileSize = 3.75 * 1024 * 1024 // 3.75MB in bytes | ||
val maxDimension = 8000 | ||
|
||
for (file in fileList as List<File>) { | ||
val validationResult = validateImageFile(file, allowedTypes, maxFileSize, maxDimension) | ||
if (validationResult != null) { | ||
errorMessages.add(validationResult) | ||
} else { | ||
validImages.add(file) | ||
} | ||
} | ||
|
||
// File count restriction | ||
if (validImages.size > 20) { | ||
errorMessages.add("A maximum of 20 images can be added to a single message.") | ||
validImages.subList(20, validImages.size).clear() | ||
} | ||
|
||
val json = OBJECT_MAPPER.writeValueAsString(validImages) | ||
browserInstance.jcefBrowser.cefBrowser.executeJavaScript( | ||
"window.handleNativeDrop('$json')", | ||
browserInstance.jcefBrowser.cefBrowser.url, | ||
0 | ||
) | ||
|
||
val errorJson = OBJECT_MAPPER.writeValueAsString(errorMessages) | ||
browserInstance.jcefBrowser.cefBrowser.executeJavaScript( | ||
"window.handleNativeNotify('$errorJson')", | ||
browserInstance.jcefBrowser.cefBrowser.url, | ||
0 | ||
) | ||
dtde.dropComplete(true) | ||
} else { | ||
dtde.dropComplete(false) | ||
} | ||
} catch (e: Exception) { | ||
LOG.error { "Failed to handle file drop operation: ${e.message}" } | ||
dtde.dropComplete(false) | ||
} | ||
} | ||
} | ||
|
||
// Set DropTarget on the browser component and its children | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how are the children involved There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We want the whole chat window to be "drag & drop zone" |
||
browserInstance.component()?.let { component -> | ||
component.dropTarget = dropTarget | ||
// Also try setting on parent if needed | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is to make sure all chat window is drop area |
||
component.parent?.dropTarget = dropTarget | ||
} | ||
|
||
initConnections() | ||
connectUi(it) | ||
connectApps(it) | ||
connectUi(browserInstance) | ||
connectApps(browserInstance) | ||
|
||
loadingPanel.stopLoading() | ||
} | ||
|
@@ -211,6 +283,36 @@ class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Di | |
} | ||
} | ||
|
||
private fun validateImageFile(file: File, allowedTypes: Set<String>, maxFileSize: Double, maxDimension: Int): String? { | ||
val fileName = file.name | ||
val ext = fileName.substringAfterLast('.', "").lowercase() | ||
|
||
if (ext !in allowedTypes) { | ||
return "$fileName: File must be an image in JPEG, PNG, GIF, or WebP format." | ||
} | ||
|
||
if (file.length() > maxFileSize) { | ||
return "$fileName: Image must be no more than 3.75MB in size." | ||
} | ||
|
||
return try { | ||
val img = read(file) | ||
when { | ||
img == null -> "$fileName: File could not be read as an image." | ||
img.width > maxDimension -> "$fileName: Image must be no more than 8,000px in width." | ||
img.height > maxDimension -> "$fileName: Image must be no more than 8,000px in height." | ||
else -> null | ||
} | ||
} catch (e: Exception) { | ||
"$fileName: File could not be read as an image." | ||
} | ||
} | ||
|
||
companion object { | ||
private val LOG = getLogger<AmazonQPanel>() | ||
private val OBJECT_MAPPER = jacksonObjectMapper() | ||
} | ||
|
||
override fun dispose() { | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -77,6 +77,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GetSe | |
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.LIST_MCP_SERVERS_REQUEST_METHOD | ||
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.LIST_RULES_REQUEST_METHOD | ||
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.MCP_SERVER_CLICK_REQUEST_METHOD | ||
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OPEN_FILE_DIALOG | ||
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OPEN_FILE_DIALOG_REQUEST_METHOD | ||
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OPEN_SETTINGS | ||
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OPEN_WORKSPACE_SETTINGS_KEY | ||
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.OpenSettingsNotification | ||
|
@@ -489,6 +491,19 @@ class BrowserConnector( | |
) | ||
} | ||
} | ||
|
||
OPEN_FILE_DIALOG -> { | ||
handleChat(AmazonQChatServer.showOpenFileDialog, node) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you walk me through the message flow? does it actually need to transit through flare before requesting the file picker? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Flow:
- does it actually need to transit through flare before requesting the file picker?
|
||
.whenComplete { response, _ -> | ||
browser.postChat( | ||
FlareUiMessage( | ||
command = OPEN_FILE_DIALOG_REQUEST_METHOD, | ||
params = response | ||
) | ||
) | ||
} | ||
} | ||
|
||
LIST_RULES_REQUEST_METHOD -> { | ||
handleChat(AmazonQChatServer.listRules, node) | ||
.whenComplete { response, _ -> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this here, can you please add a comment to the flare location that this picks up the values from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JCEF does not propagate OS-level dragenter, dragOver and drop into DOM. As an alternative, enabling the native drag in JCEF, and let the native handling the drop event, and update the UI through JS bridge.
The same logic of filtering the images are implemented on both language server and JetBrain