import type { ThreadMessage, ArtifactAction, ProposedContentRef, Artifact } from '@janhq/core'
import { useArtifacts } from '@/hooks/useArtifacts'
import { useEffect, useState } from 'react'
import { getServiceHub } from '@/hooks/useServiceHub'
import { InlineArtifactCard } from './InlineArtifactCard'
type ArtifactActionMessageProps = {
message: ThreadMessage
}
/**
* Extract artifact_action from message metadata or content
* Claude-style: AI embeds JSON in message content, we parse it
*/
function extractArtifactAction(message: ThreadMessage): ArtifactAction | undefined {
// First check metadata (preferred)
if (message.metadata?.artifact_action) {
return message.metadata.artifact_action as ArtifactAction
}
// Fallback: parse from message content
if (typeof message.content === 'string') {
try {
// Look for JSON blocks in the message
const jsonMatch = message.content.match(/```json\s*(\{[\s\S]*?\})\s*```/)
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[1])
if (parsed.artifact_action) {
return parsed.artifact_action as ArtifactAction
}
}
// Also try direct JSON parsing (in case AI outputs raw JSON)
const artifactMatch = message.content.match(/\{\s*"artifact_action"[\s\S]*?\}/)
if (artifactMatch) {
const parsed = JSON.parse(artifactMatch[0])
if (parsed.artifact_action) {
return parsed.artifact_action as ArtifactAction
}
}
} catch (err) {
// Not valid JSON, ignore
}
}
return undefined
}
export function ArtifactActionMessage({ message }: ArtifactActionMessageProps) {
const artifacts = useArtifacts()
const artifactAction = extractArtifactAction(message)
const [loadingProposal, setLoadingProposal] = useState(false)
const [artifactCreated, setArtifactCreated] = useState(false)
if (!artifactAction) return null
// Auto-create artifact when message with create action is received
useEffect(() => {
if (
artifactAction.type === 'create' &&
message.thread_id &&
!artifactCreated
) {
const { name, content_type, language, preview } = artifactAction.artifact
const content = preview || '' // Use preview as initial content
artifacts.createArtifact(message.thread_id, name, content_type, language, content)
.then(() => {
setArtifactCreated(true)
})
.catch((err) => {
console.error('Failed to auto-create artifact:', err)
})
}
}, [artifactAction, message.thread_id, artifactCreated])
// Load proposal content if it exists
useEffect(() => {
if (artifactAction.type === 'update' && artifactAction.proposed_content_ref && message.thread_id) {
const ref = artifactAction.proposed_content_ref as ProposedContentRef
// Check if we already have this proposal loaded
const existingProposal = artifacts.pendingProposals[artifactAction.artifact_id]
if (existingProposal) return
setLoadingProposal(true)
// Handle inline content vs file reference
if (ref.storage === 'inline' && ref.content) {
// Content is provided inline
artifacts.proposeUpdate(message.thread_id, artifactAction.artifact_id, ref.content)
.catch((err) => {
console.error('Failed to load inline proposal:', err)
})
.finally(() => {
setLoadingProposal(false)
})
} else if (ref.storage === 'temp' && ref.path) {
// Read the proposed content from the temp file
getServiceHub()
.artifacts()
.getArtifactContent(message.thread_id, ref.path)
.then((content) => {
if (content && message.thread_id) {
// Create a proposal
return artifacts.proposeUpdate(message.thread_id, artifactAction.artifact_id, content)
}
})
.catch((err) => {
console.error('Failed to load proposal:', err)
})
.finally(() => {
setLoadingProposal(false)
})
} else {
setLoadingProposal(false)
}
}
}, [artifactAction, message.thread_id])
const handleOpenArtifact = () => {
if (artifactAction.type === 'create' && message.thread_id) {
// For create actions, the artifact should already be created
// Just open the split view
artifacts.toggleSplitView(message.thread_id)
} else if (artifactAction.type === 'update' && message.thread_id) {
// For update actions, show the diff or open the artifact
if (!artifacts.splitViewOpen[message.thread_id]) {
artifacts.toggleSplitView(message.thread_id)
}
artifacts.setActiveArtifact(message.thread_id, artifactAction.artifact_id)
}
}
if (artifactAction.type === 'create') {
// Convert ArtifactAction to Artifact for the card
const artifactForCard: Artifact & { content: string } = {
id: artifactAction.artifact_id || 'pending',
name: artifactAction.artifact.name,
slug: artifactAction.artifact.name.toLowerCase().replace(/\s+/g, '-'),
content_type: artifactAction.artifact.content_type,
language: artifactAction.artifact.language,
content: artifactAction.artifact.preview || '',
file_path: `${artifactAction.artifact.name}.${artifactAction.artifact.language || 'txt'}`,
version: 1,
hash: '',
bytes: 0,
created_at: Date.now(),
updated_at: Date.now(),
read_only: false,
extensions: {},
metadata: {
archived: false,
tags: [],
},
}
return