支持在会话输入框直接进行文件/图片的拖拽和粘贴
This commit is contained in:
@@ -43,6 +43,13 @@ export interface UserResourceDisplay {
|
||||
mime_type?: string | null
|
||||
}
|
||||
|
||||
export interface UserImageDisplay {
|
||||
name: string
|
||||
data: string
|
||||
mime_type: string
|
||||
uri?: string | null
|
||||
}
|
||||
|
||||
const BLOCKED_RESOURCE_MENTION_RE = /@([^\s@]+)\s*\[blocked[^\]]*\]/gi
|
||||
const MARKDOWN_LINK_RE = /\[([^\]]+)\]\(([^)]+)\)/g
|
||||
|
||||
@@ -54,6 +61,7 @@ export interface AdaptedMessage {
|
||||
role: MessageRole
|
||||
content: AdaptedContentPart[]
|
||||
userResources?: UserResourceDisplay[]
|
||||
userImages?: UserImageDisplay[]
|
||||
timestamp: string
|
||||
usage?: TurnUsage | null
|
||||
duration_ms?: number | null
|
||||
@@ -398,6 +406,20 @@ function addResource(
|
||||
resources.push(resource)
|
||||
}
|
||||
|
||||
function addImage(images: UserImageDisplay[], image: UserImageDisplay) {
|
||||
const key = `${image.mime_type}:${image.data.length}:${image.data.slice(0, 64)}`
|
||||
if (
|
||||
images.some(
|
||||
(item) =>
|
||||
`${item.mime_type}:${item.data.length}:${item.data.slice(0, 64)}` ===
|
||||
key
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
images.push(image)
|
||||
}
|
||||
|
||||
export function extractUserResourcesFromText(text: string): {
|
||||
text: string
|
||||
resources: UserResourceDisplay[]
|
||||
@@ -480,6 +502,33 @@ function splitUserTextAndResources(
|
||||
return { parts: nextParts, resources }
|
||||
}
|
||||
|
||||
function deriveImageNameFromBlock(
|
||||
block: Extract<ContentBlock, { type: "image" }>
|
||||
): string {
|
||||
if (block.uri && block.uri.trim().length > 0) {
|
||||
return fileNameFromUri(block.uri)
|
||||
}
|
||||
const ext = block.mime_type.split("/")[1]?.split("+")[0] ?? "image"
|
||||
return `image.${ext}`
|
||||
}
|
||||
|
||||
function extractUserImagesFromBlocks(
|
||||
blocks: ContentBlock[]
|
||||
): UserImageDisplay[] {
|
||||
const images: UserImageDisplay[] = []
|
||||
for (const block of blocks) {
|
||||
if (block.type !== "image") continue
|
||||
if (!block.data || !block.mime_type) continue
|
||||
addImage(images, {
|
||||
name: deriveImageNameFromBlock(block),
|
||||
data: block.data,
|
||||
mime_type: block.mime_type,
|
||||
uri: block.uri ?? null,
|
||||
})
|
||||
}
|
||||
return images
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a stable tool call ID based on message ID and block index
|
||||
*/
|
||||
@@ -661,6 +710,8 @@ export function adaptMessageTurn(
|
||||
turn.role === "user"
|
||||
? splitUserTextAndResources(adaptedContent, text.attachedResources)
|
||||
: { parts: adaptedContent, resources: [] as UserResourceDisplay[] }
|
||||
const userImages =
|
||||
turn.role === "user" ? extractUserImagesFromBlocks(turn.blocks) : []
|
||||
|
||||
return {
|
||||
id: turn.id,
|
||||
@@ -668,6 +719,7 @@ export function adaptMessageTurn(
|
||||
content: userSplit.parts,
|
||||
userResources:
|
||||
userSplit.resources.length > 0 ? userSplit.resources : undefined,
|
||||
userImages: userImages.length > 0 ? userImages : undefined,
|
||||
timestamp: turn.timestamp,
|
||||
usage: turn.usage,
|
||||
duration_ms: turn.duration_ms,
|
||||
@@ -695,6 +747,7 @@ export interface MessageGroup {
|
||||
role: "user" | "assistant" | "system"
|
||||
parts: AdaptedContentPart[]
|
||||
userResources?: UserResourceDisplay[]
|
||||
userImages?: UserImageDisplay[]
|
||||
usage?: TurnUsage | null
|
||||
duration_ms?: number | null
|
||||
model?: string | null
|
||||
@@ -738,6 +791,7 @@ export function groupAdaptedMessages(
|
||||
role: effectiveRole,
|
||||
parts: [...msg.content],
|
||||
userResources: msg.userResources,
|
||||
userImages: msg.userImages,
|
||||
})
|
||||
} else {
|
||||
if (currentGroup && currentGroup.role === "assistant") {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
AdaptedContentPart,
|
||||
UserImageDisplay,
|
||||
UserResourceDisplay,
|
||||
} from "@/lib/adapters/ai-elements-adapter"
|
||||
import type { PromptDraft, PromptInputBlock } from "@/lib/types"
|
||||
@@ -10,6 +11,35 @@ function isResourceLinkBlock(
|
||||
return block.type === "resource_link"
|
||||
}
|
||||
|
||||
function isEmbeddedResourceBlock(
|
||||
block: PromptInputBlock
|
||||
): block is Extract<PromptInputBlock, { type: "resource" }> {
|
||||
return block.type === "resource"
|
||||
}
|
||||
|
||||
function isImageBlock(
|
||||
block: PromptInputBlock
|
||||
): block is Extract<PromptInputBlock, { type: "image" }> {
|
||||
return block.type === "image"
|
||||
}
|
||||
|
||||
function deriveResourceNameFromUri(uri: string): string {
|
||||
const fallback = "resource"
|
||||
const normalized = uri.trim()
|
||||
if (!normalized) return fallback
|
||||
const withoutQuery = normalized.split(/[?#]/, 1)[0]
|
||||
const candidate = withoutQuery.split(/[\\/]/).pop() ?? ""
|
||||
let decoded = ""
|
||||
if (candidate) {
|
||||
try {
|
||||
decoded = decodeURIComponent(candidate)
|
||||
} catch {
|
||||
decoded = candidate
|
||||
}
|
||||
}
|
||||
return decoded || fallback
|
||||
}
|
||||
|
||||
export function getPromptDraftDisplayText(
|
||||
draft: PromptDraft,
|
||||
attachedResourcesFallback: string
|
||||
@@ -33,9 +63,40 @@ export function buildUserMessageTextPartsFromDraft(
|
||||
export function extractUserResourcesFromDraft(
|
||||
draft: PromptDraft
|
||||
): UserResourceDisplay[] {
|
||||
return draft.blocks.filter(isResourceLinkBlock).map((resource) => ({
|
||||
const linked = draft.blocks.filter(isResourceLinkBlock).map((resource) => ({
|
||||
name: resource.name,
|
||||
uri: resource.uri,
|
||||
mime_type: resource.mime_type ?? null,
|
||||
}))
|
||||
const embedded = draft.blocks
|
||||
.filter(isEmbeddedResourceBlock)
|
||||
.map((resource) => ({
|
||||
name: deriveResourceNameFromUri(resource.uri),
|
||||
uri: resource.uri,
|
||||
mime_type: resource.mime_type ?? null,
|
||||
}))
|
||||
return [...linked, ...embedded]
|
||||
}
|
||||
|
||||
function deriveImageName(
|
||||
uri: string | null | undefined,
|
||||
mimeType: string
|
||||
): string {
|
||||
if (uri && uri.trim().length > 0) {
|
||||
const name = deriveResourceNameFromUri(uri)
|
||||
if (name !== "resource") return name
|
||||
}
|
||||
const ext = mimeType.split("/")[1]?.split("+")[0] ?? "image"
|
||||
return `image.${ext}`
|
||||
}
|
||||
|
||||
export function extractUserImagesFromDraft(
|
||||
draft: PromptDraft
|
||||
): UserImageDisplay[] {
|
||||
return draft.blocks.filter(isImageBlock).map((image) => ({
|
||||
name: deriveImageName(image.uri, image.mime_type),
|
||||
data: image.data,
|
||||
mime_type: image.mime_type,
|
||||
uri: image.uri ?? null,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -743,6 +743,13 @@ export async function stopFileTreeWatch(rootPath: string): Promise<void> {
|
||||
return invoke("stop_file_tree_watch", { rootPath })
|
||||
}
|
||||
|
||||
export async function readFileBase64(
|
||||
path: string,
|
||||
maxBytes?: number
|
||||
): Promise<string> {
|
||||
return invoke("read_file_base64", { path, maxBytes: maxBytes ?? null })
|
||||
}
|
||||
|
||||
export async function readFilePreview(
|
||||
rootPath: string,
|
||||
path: string,
|
||||
|
||||
@@ -60,6 +60,12 @@ export type MessageRole = "user" | "assistant" | "system" | "tool"
|
||||
|
||||
export type ContentBlock =
|
||||
| { type: "text"; text: string }
|
||||
| {
|
||||
type: "image"
|
||||
data: string
|
||||
mime_type: string
|
||||
uri?: string | null
|
||||
}
|
||||
| {
|
||||
type: "tool_use"
|
||||
tool_use_id: string | null
|
||||
@@ -304,8 +310,27 @@ export type ConnectionStatus =
|
||||
| "disconnected"
|
||||
| "error"
|
||||
|
||||
export interface PromptCapabilitiesInfo {
|
||||
image: boolean
|
||||
audio: boolean
|
||||
embedded_context: boolean
|
||||
}
|
||||
|
||||
export type PromptInputBlock =
|
||||
| { type: "text"; text: string }
|
||||
| {
|
||||
type: "image"
|
||||
data: string
|
||||
mime_type: string
|
||||
uri?: string | null
|
||||
}
|
||||
| {
|
||||
type: "resource"
|
||||
uri: string
|
||||
mime_type?: string | null
|
||||
text?: string | null
|
||||
blob?: string | null
|
||||
}
|
||||
| {
|
||||
type: "resource_link"
|
||||
uri: string
|
||||
@@ -430,6 +455,11 @@ export type AcpEvent =
|
||||
type: "selectors_ready"
|
||||
connection_id: string
|
||||
}
|
||||
| {
|
||||
type: "prompt_capabilities"
|
||||
connection_id: string
|
||||
prompt_capabilities: PromptCapabilitiesInfo
|
||||
}
|
||||
| {
|
||||
type: "mode_changed"
|
||||
connection_id: string
|
||||
|
||||
Reference in New Issue
Block a user