Nicholai 4e92884d51 feat: add Claude-style artifacts for persistent workspace documents
Implement a comprehensive artifacts system that allows users to create, edit,
and manage persistent documents alongside conversations, inspired by Claude's
artifacts feature.

Key Features:
- AI model integration with system prompts teaching artifact usage
- Inline preview cards in chat messages with collapsible previews
- On-demand side panel overlay (replaces split view)
- Preview/Code toggle for rendered markdown vs raw content
- Artifacts sidebar for managing multiple artifacts per thread
- Monaco editor integration for code artifacts
- Autosave with debounced writes and conflict detection
- Diff preview system for proposed updates
- Keyboard shortcuts and quick switcher
- Export/import functionality

Backend (Rust):
- New artifacts module in src-tauri/src/core/artifacts/
- 12 Tauri commands for CRUD, proposals, and export/import
- Atomic file writes with SHA256 hashing
- Sharded history storage with pruning policy
- Path traversal validation and UTF-8 enforcement

Frontend (TypeScript/React):
- ArtifactSidePanel: Claude-style overlay panel from right
- InlineArtifactCard: Preview cards embedded in chat
- ArtifactsSidebar: Floating list for artifact switching
- Enhanced ArtifactActionMessage: Parses AI metadata from content
- useArtifacts: Zustand store with autosave and conflict resolution

Types:
- Extended ThreadMessage.metadata with ArtifactAction
- ProposedContentRef supports inline and temp storage
- MIME-like content_type values for extensibility

Platform:
- Fixed platform detection to check window.__TAURI__
- Web service stubs for browser mode compatibility
- Updated assistant system prompts in both extension and web-app

This implements the complete workflow studied from Claude:
1. AI only creates artifacts when explicitly requested
2. Inline cards appear in chat with preview buttons
3. Side panel opens on demand, not automatic split
4. Users can toggle Preview/Code views and edit content
5. Autosave and version management prevent data loss
2025-11-02 12:19:36 -07:00

98 lines
2.6 KiB
TypeScript

/**
* Web implementation of ArtifactsService (minimal stub)
*
* This is a minimal implementation for browser mode.
* Most functionality requires Tauri backend.
*/
import type {
Artifact,
ArtifactIndex,
ArtifactCreate,
DiffPreview,
ImportResult,
} from '@janhq/core'
import type { ArtifactsService } from './types'
export class WebArtifactsService implements ArtifactsService {
private notImplemented(method: string): never {
throw new Error(`${method} is not implemented in web mode. Use Tauri app for full artifacts support.`)
}
async listArtifacts(_threadId: string): Promise<ArtifactIndex> {
return {
schema_version: 1,
artifacts: [],
active_artifact_id: null,
history_keep: {
max_entries: 50,
max_days: 30,
},
}
}
async createArtifact(_threadId: string, _data: ArtifactCreate): Promise<Artifact> {
return this.notImplemented('createArtifact')
}
async getArtifactContent(_threadId: string, _artifactId: string): Promise<string> {
return this.notImplemented('getArtifactContent')
}
async updateArtifact(
_threadId: string,
_artifactId: string,
_content: string,
_version: number,
_hash: string
): Promise<Artifact> {
return this.notImplemented('updateArtifact')
}
async deleteArtifact(_threadId: string, _artifactId: string): Promise<void> {
return this.notImplemented('deleteArtifact')
}
async renameArtifact(_threadId: string, _artifactId: string, _newName: string): Promise<Artifact> {
return this.notImplemented('renameArtifact')
}
async setActiveArtifact(_threadId: string, _artifactId: string | null): Promise<void> {
// No-op in web mode
return Promise.resolve()
}
async proposeUpdate(_threadId: string, _artifactId: string, _content: string): Promise<DiffPreview> {
return {
proposal_id: 'stub',
artifact_id: _artifactId,
current_version: 1,
current_hash: '',
proposed_hash: '',
hunks: [],
full_diff: '',
}
}
async applyProposal(
_threadId: string,
_artifactId: string,
_proposalId: string,
_selectedHunks?: number[]
): Promise<Artifact> {
return this.notImplemented('applyProposal')
}
async discardProposal(_threadId: string, _artifactId: string, _proposalId: string): Promise<void> {
return Promise.resolve()
}
async exportArtifacts(_threadId: string, _outputPath: string): Promise<void> {
return Promise.resolve()
}
async importArtifacts(_threadId: string, _archivePath: string): Promise<ImportResult> {
return this.notImplemented('importArtifacts')
}
}