# frozen_string_literal: true module DiscourseAi module Personas module Tools class CreateArtifact < Tool def self.name "create_artifact" end def self.specification_description <<~DESC A detailed description of the web artifact you want to create. Your specification should include: 1. Purpose and functionality 2. Visual design requirements 3. Interactive elements and behavior 4. Data handling (if applicable) 5. Specific requirements or constraints 6. DO NOT include full source code of the artifact, just very clear requirements Good specification examples: Example: (Calculator): "Create a modern calculator with a dark theme. It should: - Have a large display area showing current and previous calculations - Include buttons for numbers 0-9, basic operations (+,-,*,/), and clear - Use a grid layout with subtle hover effects on buttons - Show button press animations - Keep calculation history visible above current input - Use a monospace font for numbers - Support keyboard input for numbers and operations" Poor specification example: "Make a website that looks nice and does cool stuff" (Too vague, lacks specific requirements and functionality details) Tips for good specifications: - Be specific about layout and design preferences - Describe all interactive elements and their behavior - Include any specific visual effects or animations - Mention responsive design requirements if needed - List any specific libraries or frameworks to use/avoid - Describe error states and edge cases - Include accessibility requirements - Include code snippets to help ground the specification DESC end def self.signature { name: "create_artifact", description: "Creates a web artifact based on a specification", parameters: [ { name: "name", description: "A name for the artifact (max 255 chars)", type: "string", required: true, }, { name: "specification", type: "string", description: specification_description, required: true, }, ], } end def self.accepted_options [option(:creator_llm, type: :llm)] end def self.allow_partial_tool_calls? true end def partial_invoke if parameters[:specification].present? in_progress(specification: parameters[:specification]) end end def in_progress(specification:, source: nil) source = (<<~HTML) if source.present? ### Source ```` #{source} ```` HTML self.custom_raw = <<~HTML
Thinking... ### Specification ```` #{specification} ```` #{source}
HTML end def invoke post = Post.find_by(id: context.post_id) return error_response("No post context found") unless post partial_response = +"" artifact_code = generate_artifact_code(post: post, user: post.user) do |partial| partial_response << partial in_progress(specification: parameters[:specification], source: partial_response) yield nil, true end return error_response(artifact_code[:error]) if artifact_code[:error] artifact = create_artifact(post, artifact_code) if artifact.save update_custom_html(artifact) success_response(artifact) else self.custom_raw = self.custom_raw + "\n\n###Error creating artifact..." error_response(artifact.errors.full_messages.join(", ")) end end def chain_next_response? false end def description_args { name: parameters[:name], specification: parameters[:specification] } end private def generate_artifact_code(post:, user:) prompt = build_artifact_prompt(post: post) response = +"" llm = ( options[:creator_llm].present? && LlmModel.find_by(id: options[:creator_llm].to_i)&.to_llm ) || self.llm llm.generate( prompt, user: user, feature_name: "create_artifact", cancel_manager: context.cancel_manager, ) do |partial_response| response << partial_response yield partial_response end sections = parse_sections(response) if valid_sections?(sections) html, css, js = sections { html: html, css: css, js: js } else { error: "Failed to generate valid artifact code", response: response } end end def build_artifact_prompt(post:) DiscourseAi::Completions::Prompt.new( artifact_system_prompt, messages: [{ type: :user, content: parameters[:specification] }], post_id: post.id, topic_id: post.topic_id, ) end def parse_sections(response) sections = { html: nil, css: nil, javascript: nil } current_section = nil lines = [] response.each_line do |line| case line when /^\[(HTML|CSS|JavaScript)\]$/ current_section = line.match(/^\[(.+)\]$/)[1].downcase.to_sym lines = [] when %r{^\[/(HTML|CSS|JavaScript)\]$} sections[current_section] = lines.join if current_section current_section = nil lines = [] else lines << line if current_section end end [sections[:html].to_s.strip, sections[:css].to_s.strip, sections[:javascript].to_s.strip] end def valid_sections?(sections) return false if sections.empty? # Basic validation of sections has_html = sections[0].include?("<") && sections[0].include?(">") has_css = sections[1].include?("{") && sections[1].include?("}") has_js = sections[2].present? has_html || has_css || has_js end def create_artifact(post, code) AiArtifact.new( user_id: bot_user.id, post_id: post.id, name: parameters[:name].to_s[0...255], html: code[:html], css: code[:css], js: code[:js], metadata: { specification: parameters[:specification], }, ) end def artifact_system_prompt <<~PROMPT You are a web development expert creating HTML, CSS, and JavaScript code. Follow these instructions precisely: 1. Provide complete source code for all three required sections: HTML, CSS, and JavaScript 2. Use exact section tags: [HTML]/[/HTML], [CSS]/[/CSS], [JavaScript]/[/JavaScript] 3. Format requirements: - HTML: No , , or tags - CSS: Valid CSS rules - JavaScript: Clean, working code 4. NEVER USE SHORTCUTS - generate complete code for each section. No placeholders. 5. If you need to source ANY 3rd party libraries, use the following CDNs: #{AiArtifact::ALLOWED_CDN_SOURCES.join("\n")} 6. When sourcing libraries, include them in the [HTML] section, for example: Required response format: [HTML]
[/HTML] [CSS] #app { /* Your complete CSS here */ } [/CSS] [JavaScript] // Your complete JavaScript here [/JavaScript] Important: - All three sections are required - Sections must use exact tags shown above - Focus on simplicity and reliability - Include basic error handling - Follow accessibility guidelines - No explanatory text, only code PROMPT end def update_custom_html(artifact) html_preview = <<~MD [details="View Source"] ### HTML ```html #{artifact.html} ``` ### CSS ```css #{artifact.css} ``` ### JavaScript ```javascript #{artifact.js} ``` [/details] ### Preview
MD self.custom_raw = html_preview end def success_response(artifact) { status: "success", artifact_id: artifact.id, message: "Artifact created successfully." } end def error_response(message) { status: "error", error: message } end end end end end