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
168 lines
5.1 KiB
TypeScript
168 lines
5.1 KiB
TypeScript
import { Assistant, AssistantExtension, fs, joinPath } from '@janhq/core'
|
||
export default class JanAssistantExtension extends AssistantExtension {
|
||
async onLoad() {
|
||
if (!(await fs.existsSync('file://assistants'))) {
|
||
await fs.mkdir('file://assistants')
|
||
}
|
||
const assistants = await this.getAssistants()
|
||
if (assistants.length === 0) {
|
||
await this.createAssistant(this.defaultAssistant)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Called when the extension is unloaded.
|
||
*/
|
||
onUnload(): void {}
|
||
|
||
async getAssistants(): Promise<Assistant[]> {
|
||
if (!(await fs.existsSync('file://assistants')))
|
||
return [this.defaultAssistant]
|
||
const assistants = await fs.readdirSync('file://assistants')
|
||
const assistantsData: Assistant[] = []
|
||
for (const assistant of assistants) {
|
||
const assistantPath = await joinPath([
|
||
'file://assistants',
|
||
assistant,
|
||
'assistant.json',
|
||
])
|
||
if (!(await fs.existsSync(assistantPath))) {
|
||
console.warn(`Assistant file not found: ${assistantPath}`)
|
||
continue
|
||
}
|
||
try {
|
||
const assistantData = JSON.parse(await fs.readFileSync(assistantPath))
|
||
assistantsData.push(assistantData as Assistant)
|
||
} catch (error) {
|
||
console.error(`Failed to read assistant ${assistant}:`, error)
|
||
}
|
||
}
|
||
return assistantsData
|
||
}
|
||
|
||
async createAssistant(assistant: Assistant): Promise<void> {
|
||
const assistantPath = await joinPath([
|
||
'file://assistants',
|
||
assistant.id,
|
||
'assistant.json',
|
||
])
|
||
const assistantFolder = await joinPath(['file://assistants', assistant.id])
|
||
if (!(await fs.existsSync(assistantFolder))) {
|
||
await fs.mkdir(assistantFolder)
|
||
}
|
||
await fs.writeFileSync(assistantPath, JSON.stringify(assistant, null, 2))
|
||
}
|
||
|
||
async deleteAssistant(assistant: Assistant): Promise<void> {
|
||
const assistantPath = await joinPath([
|
||
'file://assistants',
|
||
assistant.id,
|
||
'assistant.json',
|
||
])
|
||
if (await fs.existsSync(assistantPath)) {
|
||
await fs.rm(assistantPath)
|
||
}
|
||
}
|
||
|
||
private defaultAssistant: Assistant = {
|
||
avatar: '👋',
|
||
thread_location: undefined,
|
||
id: 'jan',
|
||
object: 'assistant',
|
||
created_at: Date.now() / 1000,
|
||
name: 'Jan',
|
||
description:
|
||
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf.',
|
||
model: '*',
|
||
instructions:
|
||
`You are a helpful AI assistant. Your primary goal is to assist users with their questions and tasks to the best of your abilities.
|
||
|
||
When responding:
|
||
- Answer directly from your knowledge when you can
|
||
- Be concise, clear, and helpful
|
||
- Admit when you're unsure rather than making things up
|
||
|
||
If tools are available to you:
|
||
- Only use tools when they add real value to your response
|
||
- Use tools when the user explicitly asks (e.g., "search for...", "calculate...", "run this code")
|
||
- Use tools for information you don't know or that needs verification
|
||
- Never use tools just because they're available
|
||
|
||
When using tools:
|
||
- Use one tool at a time and wait for results
|
||
- Use actual values as arguments, not variable names
|
||
- Learn from each result before deciding next steps
|
||
- Avoid repeating the same tool call with identical parameters
|
||
|
||
Remember: Most questions can be answered without tools. Think first whether you need them.
|
||
|
||
Artifacts - Persistent Workspace Documents:
|
||
|
||
When the user needs to create, edit, or iterate on substantial content (code, documents, structured data), you can use artifacts to provide a persistent workspace alongside the conversation.
|
||
|
||
When to create artifacts:
|
||
- User explicitly requests ("put this in an artifact", "create a document", "save this")
|
||
- Content is substantial and likely to be edited (>15 lines of code, documents, structured data)
|
||
- User signals intent to iterate ("so I can edit it", "we can refine", "I want to modify")
|
||
|
||
When NOT to create artifacts:
|
||
- Simple Q&A responses
|
||
- Short explanations or examples
|
||
- Content user hasn't signaled they want to save
|
||
|
||
To create an artifact, include this JSON in your response:
|
||
{
|
||
"artifact_action": {
|
||
"type": "create",
|
||
"artifact_id": "unique-id",
|
||
"artifact": {
|
||
"name": "Descriptive Name",
|
||
"content_type": "text/markdown",
|
||
"language": "markdown",
|
||
"preview": "full content here"
|
||
}
|
||
}
|
||
}
|
||
|
||
To update an artifact:
|
||
{
|
||
"artifact_action": {
|
||
"type": "update",
|
||
"artifact_id": "existing-id",
|
||
"changes": {
|
||
"description": "Added timeline section"
|
||
},
|
||
"proposed_content_ref": {
|
||
"storage": "inline",
|
||
"content": "updated full content here"
|
||
}
|
||
}
|
||
}
|
||
|
||
Always announce artifact creation: "I'll create a [type] artifact for [purpose]!"
|
||
|
||
Current date: {{current_date}}`,
|
||
tools: [
|
||
{
|
||
type: 'retrieval',
|
||
enabled: false,
|
||
useTimeWeightedRetriever: false,
|
||
settings: {
|
||
top_k: 2,
|
||
chunk_size: 1024,
|
||
chunk_overlap: 64,
|
||
retrieval_template: `Use the following pieces of context to answer the question at the end.
|
||
----------------
|
||
CONTEXT: {CONTEXT}
|
||
----------------
|
||
QUESTION: {QUESTION}
|
||
----------------
|
||
Helpful Answer:`,
|
||
},
|
||
},
|
||
],
|
||
file_ids: [],
|
||
metadata: undefined,
|
||
}
|
||
}
|