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 } if (artifactAction.type === 'update') { return (
Updated artifact
{artifactAction.changes.description}
{artifactAction.changes.diff_preview && (
{artifactAction.changes.diff_preview}
)}
) } return null }