This guide provides step-by-step instructions on how to:
- Use the RADARs prototype
- Build a similar Webflow-based web app for AI transparency reporting
- 1️⃣ Access the Live Prototype
- 2️⃣ How to Build Your Own Web App
- Set Up a Webflow Site
- Set Up a Firebase Database
- Automate Data Processing with Zapier
- Zap 1: Webflow Form Submission
- Zap 2: Generate Unique Slug
- Zap 3: Webflow CMS - Create Live Item
- Zap 4: Firebase Firestore - Store User Responses
- Zap 5: AI Report Generation
- Zap 6: Validate AI-Generated Reports
- Zap 7: Format AI-Generated Reports
- Zap 8-10: Store User-Editable Content in Firebase
- Zap 11: Calculate AI Risk Scores
- Zap 12: Store Scores in Firebase
- Zap 13: Match Practice with AI Risk Frameworks
- Zap 14: Provide Responsible AI Recommendations
- Zap 15: Validate Recommendations
- Zap 16: Process and Format Validated Recommendations
- Zap 17: Store Customized Recommendations
- Zap 18: Update Report & Recommendations in Webflow
- Zap 19: Update Response Status in Firebase Firestore
- 4️⃣ Build a Survey Flow
- 5️⃣ Configure AI Report
- 6️⃣ Deploy Your Webflow App
You can explore and test the RADARs prototype here:
👉 Live Prototype
- Enter Basic Information – Provide details about your business domain, AI model, and audience.
- Complete the Tailored Questionnaire – Answer questions related to AI practices, such as data handling and ethical risks.
- Generate Transparency Report – Click Generate Report to receive a comprehensive AI transparency assessment.
- Sign up for Webflow.
- Create your Workspace, then create a new site.
- Create Landing Page.
- Create Questionaire Page.
- This page is where users can input basic information of their business to get customized recommendations of the most relevant risky areas for their business regarding their AI practices.
- We used webflow "Form block" to contain all the questions.

- Note that you will need to put all your question cards inside a single form block to collect all the user inputs altogether for next steps.
- Add question cards. You can customize your own survey/question list.
-
Dropdown Choice: Add "Select" from "Forms" section to the page. Configure your choice texts and values in "Settings".

-
Fill in Blank: Add "Input" from "Forms" section to the page. Configure the Text Feild Name, Type, Placeholder, and if "Required" in "Settings".
If longer input from users is needed, use "Text Area" instead. Configure the Textare Name, Placeholder, and if "Required" in "Settings".

-
Single Choice: Add "Radio Button" from "Forms" section to the page. Configure the Radio Button "Group Name"(Question Name), Choice Value, and if "Required" in "Settings".
To change the choice display text, select the corresponding "form_radio-label" from Navigator, and configure Radio Button Label Text in "Settings".

-
Multi Choice:
We used custom code from Memberstack. You can find detailed instructions here. Configure the choice values in "Custom attributes", add Name as "ms-code-select-options", Value in format like "Commercial License,Proprietary,Public Domain,Fair Use Claim,Other", seperating each choice with a single ",".

-
- Create Report Page
- Customize the page designs to match your brand.
RADARs uses Firebase Firestore to store user inputs and reports.
-
Go to Firebase Console.
-
Create a Firebase project and enable Firestore.
-
Set rules to allow authenticated users to read/write.
- For Accessment Score, the rules are:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // Allows any user (with session-based ID) to write responses match /users/{sessionId}/responses/{sectionId} { allow read, write; } // Benchmark scores (highest possible section scores) match /sections/{sectionId} { allow read; // Everyone can read max scores allow write; } } } - For Report Generation, we didn't set any specific rules.
- For Accessment Score, the rules are:
-
Connect app with project. We connected Zapier with the Report Generation project for automation.

-
Configure Functions when needed. For project Report Generation, we set up two functions:

1️⃣ Get Report Data
- The
getReportDatafunction is a Firebase Cloud Function that retrieves a generated AI transparency report based on a given slug (unique identifier for a report). It queries Firestore and returns relevant report metadata. -
- Accepts a slug as a query parameter (
?slug=your-report-slug). - Searches for a document in Firestore under the
Response_Statuscollection whereslugmatches. - If no matching document is found, it returns
{ error: "Document not found" }. - If a report exists, it retrieves:
webflowItemId(for Webflow integration).userEmail(associated with the report).slug(report identifier).
- Implements CORS headers to allow secure communication between Webflow and Firebase.
- Accepts a slug as a query parameter (
-
In your functions/index.js, add the following function:/** * Import function triggers from their respective submodules: * * const {onCall} = require("firebase-functions/v2/https"); * const {onDocumentWritten} = require("firebase-functions/v2/firestore"); * * See a full list of supported triggers at https://firebase.google.com/docs/functions */ /* const functions = require("firebase-functions"); const admin = require("firebase-admin"); const cors = require("cors")({ origin: ["https://brightpath-report.webflow.io", "https://www.brightpath-report.webflow.io"], credentials: true, }); admin.initializeApp(); // checkStatus 函数 exports.checkStatus = functions.https.onRequest(async (req, res) => { return cors(req, res, async () => { res.set("Access-Control-Allow-Origin", "https://brightpath-report.webflow.io"); res.set("Access-Control-Allow-Credentials", "true"); if (req.method === "OPTIONS") { res.set("Access-Control-Allow-Methods", "GET"); res.set("Access-Control-Allow-Headers", "Content-Type,Accept"); res.status(204).send(""); return; } const email = req.query.email; if (!email) { return res.json({ exists: false, completed: false, }); } try { const docRef = admin.firestore() .collection("Response_Status") .doc(email); const docSnap = await docRef.get(); if (!docSnap.exists) { return res.json({ exists: false, completed: false, }); } else { const data = docSnap.data(); if (data.completed === "true" && data.Item_ID) { return res.json({ exists: true, completed: true, slug: data.slug || "", webflowItemId: data.Item_ID, }); } else { return res.json({ exists: true, completed: false, }); } } } catch (error) { console.error("Error checking status:", error); return res.status(500).json({ error: "Internal server error", details: error.message, }); } }); }); // getReportData 函数 exports.getReportData = functions.https.onRequest(async (req, res) => { cors(req, res, async () => { const slug = req.query.slug; if (!slug) { return res.status(400).json({error: "Slug is required"}); } try { const snapshot = await admin.firestore() .collection("Response_Status") .where("slug", "==", slug) .limit(1) .get(); if (snapshot.empty) { return res.status(404).json({error: "Document not found"}); } const doc = snapshot.docs[0]; const data = doc.data(); return res.json({ webflowItemId: data.Item_ID, userEmail: doc.id, completed: data.completed === "true", slug: data.slug, }); } catch (error) { console.error("Error:", error); return res.status(500).json({ error: "Server error", details: error.message, }); } }); }); */ const functions = require("firebase-functions"); const admin = require("firebase-admin"); const cors = require("cors")({origin: true}); admin.initializeApp(); // checkStatus 函数 exports.checkStatus = functions.https.onRequest(async (req, res) => { // 设置允许所有来源的访问 res.set("Access-Control-Allow-Origin", "*"); res.set("Access-Control-Allow-Methods", "GET, OPTIONS"); res.set("Access-Control-Allow-Headers", "Content-Type"); if (req.method === "OPTIONS") { res.status(204).send(""); return; } const email = req.query.email; if (!email) { return res.json({exists: false}); } try { const docRef = admin.firestore().collection("Response_Status").doc(email); const docSnap = await docRef.get(); if (!docSnap.exists) { return res.json({exists: false}); } const data = docSnap.data(); return res.json({ exists: true, slug: data.slug || "", webflowItemId: data.Item_ID || "", }); } catch (error) { console.error("Error checking status:", error); return res.status(500).json({ error: "Internal server error", details: error.message, }); } }); // getReportData 函数 exports.getReportData = functions.https.onRequest(async (req, res) => { cors(req, res, async () => { const slug = req.query.slug; if (!slug) { return res.status(400).json({error: "Slug is required"}); } try { const snapshot = await admin.firestore() .collection("Response_Status") .where("slug", "==", slug) .limit(1) .get(); if (snapshot.empty) { return res.status(404).json({error: "Document not found"}); } const doc = snapshot.docs[0]; const data = doc.data(); return res.json({ webflowItemId: data.Item_ID, userEmail: doc.id, slug: data.slug, }); } catch (error) { console.error("Error:", error); return res.status(500).json({ error: "Server error", details: error.message, }); } }); });
- The
2️⃣ Check Report Generation Status
- The
checkStatusfunction is a Firebase Cloud Function used to verify if a report has been generated for a given user email. It interacts with Firestore to check the status of a user's response and returns relevant metadata. -
- Accepts an email as a query parameter.
- Looks for the corresponding document in Firestore under
Response_Status/{email}. - If no document is found, it returns
{ exists: false }. - If the document exists:
- Retrieves the slug and Webflow item ID.
- Returns
{ exists: true, slug, webflowItemId }.
- Implements CORS headers to allow secure communication between Webflow and Firebase.
-
In your functions/index.js, add the following function:/** * Import function triggers from their respective submodules: * * const {onCall} = require("firebase-functions/v2/https"); * const {onDocumentWritten} = require("firebase-functions/v2/firestore"); * * See a full list of supported triggers at https://firebase.google.com/docs/functions */ /* const functions = require("firebase-functions"); const admin = require("firebase-admin"); const cors = require("cors")({ origin: ["https://brightpath-report.webflow.io", "https://www.brightpath-report.webflow.io"], credentials: true, }); admin.initializeApp(); // checkStatus 函数 exports.checkStatus = functions.https.onRequest(async (req, res) => { return cors(req, res, async () => { res.set("Access-Control-Allow-Origin", "https://brightpath-report.webflow.io"); res.set("Access-Control-Allow-Credentials", "true"); if (req.method === "OPTIONS") { res.set("Access-Control-Allow-Methods", "GET"); res.set("Access-Control-Allow-Headers", "Content-Type,Accept"); res.status(204).send(""); return; } const email = req.query.email; if (!email) { return res.json({ exists: false, completed: false, }); } try { const docRef = admin.firestore() .collection("Response_Status") .doc(email); const docSnap = await docRef.get(); if (!docSnap.exists) { return res.json({ exists: false, completed: false, }); } else { const data = docSnap.data(); if (data.completed === "true" && data.Item_ID) { return res.json({ exists: true, completed: true, slug: data.slug || "", webflowItemId: data.Item_ID, }); } else { return res.json({ exists: true, completed: false, }); } } } catch (error) { console.error("Error checking status:", error); return res.status(500).json({ error: "Internal server error", details: error.message, }); } }); }); // getReportData 函数 exports.getReportData = functions.https.onRequest(async (req, res) => { cors(req, res, async () => { const slug = req.query.slug; if (!slug) { return res.status(400).json({error: "Slug is required"}); } try { const snapshot = await admin.firestore() .collection("Response_Status") .where("slug", "==", slug) .limit(1) .get(); if (snapshot.empty) { return res.status(404).json({error: "Document not found"}); } const doc = snapshot.docs[0]; const data = doc.data(); return res.json({ webflowItemId: data.Item_ID, userEmail: doc.id, completed: data.completed === "true", slug: data.slug, }); } catch (error) { console.error("Error:", error); return res.status(500).json({ error: "Server error", details: error.message, }); } }); }); */ const functions = require("firebase-functions"); const admin = require("firebase-admin"); const cors = require("cors")({origin: true}); admin.initializeApp(); // checkStatus 函数 exports.checkStatus = functions.https.onRequest(async (req, res) => { // 设置允许所有来源的访问 res.set("Access-Control-Allow-Origin", "*"); res.set("Access-Control-Allow-Methods", "GET, OPTIONS"); res.set("Access-Control-Allow-Headers", "Content-Type"); if (req.method === "OPTIONS") { res.status(204).send(""); return; } const email = req.query.email; if (!email) { return res.json({exists: false}); } try { const docRef = admin.firestore().collection("Response_Status").doc(email); const docSnap = await docRef.get(); if (!docSnap.exists) { return res.json({exists: false}); } const data = docSnap.data(); return res.json({ exists: true, slug: data.slug || "", webflowItemId: data.Item_ID || "", }); } catch (error) { console.error("Error checking status:", error); return res.status(500).json({ error: "Internal server error", details: error.message, }); } }); // getReportData 函数 exports.getReportData = functions.https.onRequest(async (req, res) => { cors(req, res, async () => { const slug = req.query.slug; if (!slug) { return res.status(400).json({error: "Slug is required"}); } try { const snapshot = await admin.firestore() .collection("Response_Status") .where("slug", "==", slug) .limit(1) .get(); if (snapshot.empty) { return res.status(404).json({error: "Document not found"}); } const doc = snapshot.docs[0]; const data = doc.data(); return res.json({ webflowItemId: data.Item_ID, userEmail: doc.id, slug: data.slug, }); } catch (error) { console.error("Error:", error); return res.status(500).json({ error: "Server error", details: error.message, }); } }); });
- Copy Firebase configuration and store it in a
.envfile (if applicable).
Zapier automates the connection between Webflow, Firebase, and AI processing. This allows user-submitted data to flow through the system and generate AI transparency reports.
- A Zapier account (Sign up here)
- A Webflow project with a form connected
- A Firebase Firestore database set up
- Sign up for Zapier.
- Create the following Zaps
- Test your Zapier workflows.
- Go to Zapier and create a new Zap.
- Select "Webhooks by Zapier" as the trigger app.
- Choose "Catch Hook" as the trigger event.
- Copy the Webhook URL provided by Zapier.

- Go to Webflow and navigate to the form settings.
- In the "Form Block Settings" section "Connections", paste the Zapier Webhook URL in "Action".

- Set the method to POST.
- Save & Publish your Webflow project.
- Return to Zapier and test the trigger by submitting the Webflow form. 📌 Expected Outcome: When a user submits the form in Webflow, Zapier captures the data.
This step generates a unique slug based on the user's name. The slug is used as a unique identifier for reports and database records.

- In Zapier, add a new action.
- Select "Code by Zapier" as the action app.
- Choose "Run JavaScript" as the action event.
- Click Continue to configure the code step.
- Under Input Data, set the following:
- Key:
userName - Value: Select the user's name from the Webflow form submission (Step 1).
- Key:
- In the code editor, paste the following JavaScript code:
// 1) Get user input (e.g., userName = "Jane"): let userName = inputData.userName; // For example, "Jane" // 2) Perform slugify (convert spaces to "-", lowercase, etc.) function slugify(str) { return str .toLowerCase() .replace(/[^a-z0-9]+/g, '-') // Convert non-alphanumeric characters to "-" .replace(/^-|-$/g, '') // Remove leading and trailing "-" .substring(0, 30); // Limit to a maximum of 30 characters } let baseSlug = slugify(userName); // "jane" // 3) To ensure uniqueness, add a random suffix let random = Math.floor(Math.random() * 1000); // 0~999 let finalSlug = baseSlug + "-" + random; // "jane-543" // Output return { finalSlug }; - Click Continue, then Test & Review to ensure the function works correctly.
This step sends user input data (including the generated slug) to Webflow CMS to store user reports.
- In Zapier, add a new action.
- Select "Webflow" as the app.
- Choose "Create Live Item" as the action event.
- Click Continue to configure the setup.
- Connect your Webflow account (if not already connected).
- Under "Site", select the Webflow project where the reports will be stored.
- Under "Collection", select the correct Webflow CMS Collection (e.g., "Back-ends" in your setup).
📌 Test and Publish
- Click Test & Review to verify that the data is correctly sent to Webflow.
- Check Webflow CMS to confirm the new entry appears.
- Click Continue and publish the Zap.
This step saves user responses from Webflow into Firebase Firestore for storage and later report generation.
- In Zapier, add a new action.
- Select "Firebase Firestore" as the app.
- Choose "Create Cloud Firestore Document" as the action event.
- Click Continue to configure the setup.
- Connect your Firebase account (if not already connected).
- Under "Collection", enter the Firestore collection where the data should be stored:
✅ Example:Question_Answer - Under "Document ID", set a unique identifier for each user submission:
✅ Example: Use User Email (testingfelicity01@gmail.com)
This ensures each submission is stored under the corresponding user’s document.
- Under "Data", map all user responses you need to Firestore fields.
- Click Test & Review to verify that the data is correctly stored in Firestore.

This step sends user responses to Claude AI to generate a structured AI transparency report. Here's the example of Anthropic (Claude) API call.

- In Zapier, add a new action.
- Select "Anthropic (Claude)" as the app.
- Choose "Send Message" as the action event.
- Connect your Claude AI account.
- Configure the request:
- Use the user’s responses (from Firebase) as input.
- Ensure the output is in structured JSON format.
- Include instructions to strictly follow user input and not generate assumptions.
- Run a test to confirm Claude generates a valid report.
- Check the output for proper formatting.
- Publish the Zap to automate report generation.
This step ensures the AI-generated report accurately reflects user input while improving professionalism and compliance. Here's the example of Anthropic (Claude) API call.

- In Zapier, add a new action.
- Select "Anthropic (Claude)" as the app.
- Choose "Send Message" as the action event.
- Connect your Claude AI account.
- Configure the request:
- Send the initial AI-generated report (from Zap 5).
- Include user input for reference.
- Instruct the AI to match user input exactly, correct errors, and enhance clarity.
- Run a test to confirm Claude refines the report accurately.
- Check the output to ensure it aligns with user input.
- Publish the Zap to automate report verification.
This step ensures the AI-generated report is correctly structured in JSON format before storing it in Firebase.
- Add a "Code by Zapier" Action
- Select Run JavaScript as the action event.
- Process AI Output
// Parse the input data let rawInput = inputData.json_input; // Process and format the JSON try { // Parse the JSON data let parsedData = JSON.parse(rawInput); // Format the JSON with indentation (without adding extra escape characters) let formattedJson = JSON.stringify(parsedData, null, 2); // Return the valid JSON object, not as a string return JSON.parse(formattedJson); } catch (error) { // If JSON parsing fails, return an error message return { error: "Invalid JSON format" }; } - Handle Errors
- If the AI response is not in a valid JSON format, return an error message.
- Click Continue, then Test & Review to ensure the JSON is correctly formatted.

This step saves user-editable sections of the AI-generated report to Firebase Firestore for structured storage and retrieval.

- In Zapier, add a new action.
- Select "Firebase / Firestore" as the app.
- Choose "Create Cloud Firestore Document" as the action event.
- Connect your Firebase account.
- Configure the request:
- Set Collection to the relevant section (e.g.,
Generated_[Section-Name]). - Use the user's email as the Document ID.
- Map the necessary fields (e.g., Industry, Product Details, Transparency Info) from the structured AI-generated report.
- Set Collection to the relevant section (e.g.,
- Run a test to confirm that the document is successfully created in Firestore.
- Check the output to ensure it contains the correct user-editable content.
- Publish the Zap to store and manage the editable report sections dynamically.
This step analyzes user responses, extracts relevant sections, and assigns risk scores based on predefined criteria, helping evaluate compliance levels dynamically. It categorizes risks as High, Medium, or Low and structures the results for further processing or storage.
- In Zapier, add a new action.
- Select "Code by Zapier" as the app.
- Choose "Run JavaScript" as the action event.
- Click Continue to configure the code step.
- Under Input Data, set:
- User ID: Retrieve from user responses.
- Relevant Fields: Map compliance-related answers (e.g., privacy, security, data-sharing).

- In the code editor, paste the following JavaScript code:
let sectionScores = {};
let sectionSeverity = {};
let sectionNames = {};
let userId = inputData["user_id"] || "unknown_user";
console.log("Received data:", JSON.stringify(inputData, null, 2));
for (let key in inputData) {
if (key === "user_id" || !inputData[key] || inputData[key].includes("--placeholder--")) continue;
let value = inputData[key];
console.log("Processing field:", key, "Value:", value);
// Extract section name and question number (e.g., "Privacy & Security_2_2")
let sectionMatch = key.match(/Case \d+ ([^_]+)_\d+_\d+/);
if (sectionMatch) {
let sectionName = sectionMatch[1]; // Extracted section name (e.g., "Privacy & Security")
// Ensure section exists in the tracking objects
if (!sectionScores[sectionName]) {
sectionScores[sectionName] = 0;
sectionSeverity[sectionName] = "Medium";
sectionNames[sectionName] = sectionName;
}
// Extract score (if "|1" exists)
let scoreMatch = value.match(/\|(\d+)$/);
if (scoreMatch) {
let score = parseInt(scoreMatch[1], 10);
sectionScores[sectionName] += score;
}
// Detect High Risk (Improved method)
if (value.toLowerCase().includes("high risk")) {
sectionSeverity[sectionName] = "High";
}
}
}
// **Ensure all sections have correct severity levels**
for (let section in sectionScores) {
if (sectionScores[section] === 0) {
sectionSeverity[section] = "Low"; // If no bad practices are detected, set to "Low"
} else if (!sectionSeverity[section]) {
sectionSeverity[section] = "Medium"; // Default to "Medium" if no "High Risk" found
}
}
// Output result formatted for Firebase
output = {
user_id: userId,
section_scores: sectionScores,
section_severity: sectionSeverity,
section_names: sectionNames
};
- Click Continue, then Test & Review to verify the scoring process.
- Check the output:
- Scores are assigned to each section.
- Severity levels (Low, Medium, High) are correctly categorized.

- Publish the Zap to automate the risk scoring process.
This step saves the calculated scores for each section into Firebase Firestore for future reference and further analysis.
- In Zapier, add a new action.
- Select "Firebase / Firestore" as the action app.
- Choose "Create Cloud Firestore Document" as the action event.
- Connect your Firebase Firestore account.
- Configure the request containing Collection:
User Scores(stores the section scores), Document ID:user_id(ensures scores are linked to the correct user), and Data Fields:

- Run a test to confirm the scores are correctly stored in Firestore.

This step compares user responses about AI practices with Microsoft's Responsible AI Standards to identify potential risks and alignment gaps. The output helps generate tailored recommendations.
- In Zapier, add a new action.
- Select "Code by Zapier" as the action app.
- Choose "Run Python" as the action event.
- Click Continue to configure the code step.
- Under Input Data, set the following:
- In the code editor, process the user responses:
- Extract relevant responses regarding data privacy, bias mitigation, and AI risk factors.
- Match these responses against Microsoft's Responsible AI Standard documentation.
- Identify risk areas and gaps in compliance.
- Format output as structured Markdown text.
import re import json def parse_user_input_lines(user_input_raw): """ Parse multi-line string into question->answer pairs. If it contains |1, record it as bad_practice. The regex assumes each line is in a format like: 2.1. Some question ...? {{someAnswer}} extra If the format differs, the pattern may need adjustments. """ user_dict = {} bad_practices = {} pattern = re.compile(r'^(.*?)\{\{(.*?)\}\}(.*)$') # Split into 3 groups: # (1) question_part => (.*?) up to {{ # (2) braces => (.*?) inside the braces # (3) remainder => (.*)$ possibly containing |1 lines = user_input_raw.splitlines() line_index = 0 for line in lines: line_index += 1 line = line.strip() if not line or line.startswith("-----"): # Skip empty lines & "----- Start of case..." continue match = pattern.match(line) if match: question_part = match.group(1).strip() inside_braces = match.group(2).strip() remainder = match.group(3).strip() raw_answer = (inside_braces + " " + remainder).strip() else: question_part = f"Line{line_index}" raw_answer = line # Check for |1 is_bad = ("|1" in line) clean_answer = raw_answer.replace("|1", "").strip() # Store question->answer pair key = f"Line{line_index}: {question_part}" user_dict[key] = clean_answer if is_bad: bad_practices[key] = clean_answer return user_dict, bad_practices def approx_token_count(text): """ Simple approximation: In common English/ASCII text, 1 token ~ 3-4 characters. Here, we assume every 4 characters ~ 1 token. """ return int(len(text) / 4) + 1 def main(input_data): # 1) Read the input passed from Zapier user_input_raw = input_data.get("user_input", "").strip() markdown_text = input_data.get("markdown_text", "") print("DEBUG: user_input_raw =", repr(user_input_raw)) # 2) Parse multi-line input => user_dict, bad_practices user_dict, bad_practices = parse_user_input_lines(user_input_raw) print("DEBUG: user_dict =", user_dict) print("DEBUG: bad_practices =", bad_practices) # 3) Define keywords to search in markdown_text keywords = { "PII": ["personal identifiable information", "PII", "user data", "sensitive data"], "Data Sharing": ["third party", "vendor", "data sharing", "external AI", "uploaded files"], "AI Fairness": ["bias", "fairness", "discrimination", "ethics"], "Transparency": ["explainability", "decision making", "black box", "accountability"], "HIGH RISK": ["High Risk"], "MEDIUM RISK": ["Medium Risk"] } # 4) Match snippets matched_sections = {} # Token Limit -> you can adjust as needed (Claude 1/2 typical ~ 8k-100k tokens) # Here, we use 4000 tokens as a safe limit for demonstration MAX_TOKENS = 4000 total_tokens_used = 0 for question_key, answer_text in bad_practices.items(): matched_sections[question_key] = [] # Remove duplicates used_snippets = set() for category, word_list in keywords.items(): for word in word_list: pattern2 = re.compile(re.escape(word), re.IGNORECASE) matches = [m.start() for m in pattern2.finditer(markdown_text)] for match_start in matches: # Option 1: Shorten snippet (+300 characters) snippet_start = max(0, match_start - 80) snippet_end = min(len(markdown_text), match_start + 300) snippet = markdown_text[snippet_start:snippet_end] # Option 3: Remove duplicates if snippet in used_snippets: continue used_snippets.add(snippet) # Option 5: Token limit snippet_tokens = approx_token_count(snippet) if total_tokens_used + snippet_tokens > MAX_TOKENS: # If exceeding limit -> break and stop adding snippets break else: total_tokens_used += snippet_tokens # Append snippet matched_sections[question_key].append({ "category": category, "keyword": word, "matched_text": snippet }) # Check if token limit is reached if total_tokens_used >= MAX_TOKENS: break if total_tokens_used >= MAX_TOKENS: break # 5) Construct prompt claude_prompt = ( "## User's Bad Practices:\n" f"{json.dumps(bad_practices, indent=2)}\n\n" "## Related Microsoft AI Standard Sections:\n" f"{json.dumps(matched_sections, indent=2)}" ) # 6) Output results output = { "prompt": claude_prompt, "bad_practices": bad_practices, "matched_sections": matched_sections, "parsed_lines": user_dict } return output # Zapier code by Zapier entry point result = main(input_data) return result - Click Continue, then Test & Review to validate the matching process.
- Check that the output includes matched risks and reference sections from Microsoft’s standard.

This Zap uses API Calls to analyze AI practices flagged as risky and generate structured improvement recommendations based on responsible AI frameworks.Here's the example of Claude.
- In Zapier, add a new action.
- Select "Anthropic (Claude)" as the action app.
- Choose "Send Message" as the action event.
- Click Continue to configure the message prompt.
- Under User Message, set the following:
This step validates the AI-generated recommendations to ensure they accurately align with the Matched Responsible AI Standard requirements. It verifies correctness, notes inaccuracies, and refines the output for final analysis. You can add multiple API calls for validation purpose. (recommended)

- In Zapier, add a new action.
- Select "Anthropic (Claude)" as the action app.
- Choose "Send Message" as the action event.
- Click Continue to configure the message prompt.
- Under User Message, set the following:
- Expert Recommendations: Use the
analysis_resultsfield from Zap 14, containing generated recommendations. - Raw User RiskyPractices with Matched AI Standard: Use the
promptfield from Zap 13, ensuring validation against original risk areas.
- Expert Recommendations: Use the
This step processes the validated Responsible AI recommendations from the previous step, formats them into structured text, and prepares them for storage and display in reports.

- In Zapier, add a new action.
- Select "Code by Zapier" as the action app.
- Choose "Run Javascript" as the action event.
- Click Continue to configure the code step.
- Under Input Data, set the following:
- In the code editor, add code that parses, processes, and structures the validated AI recommendations:
- Extracts relevant details: AI practice, associated risks, and suggested improvements.
- Formats recommendations into structured text, ensuring readability and clarity.
- Filters out "No risky practices identified" entries to avoid clutter.
- Adds numbering (e.g., "analysis_results 1", "analysis_results 2") for structured outputs.
// 1) Retrieve the raw JSON string from the previous LLM step const rawJson = inputData.raw_json_output; try { // 2) Parse the raw string into a JS object const parsed = JSON.parse(rawJson); // We'll build a new object "outputFields" so that each array item becomes // "analysis_results 1", "analysis_results 2", etc. let outputFields = {}; // If we have an array: parsed.analysis_results if (parsed.analysis_results && Array.isArray(parsed.analysis_results)) { parsed.analysis_results.forEach((item, index) => { // The key "analysis_results 1" or "analysis_results 2" etc. const keyName = `analysis_results ${index + 1}`; // Extract fields const section = item["section"] || ""; const practice = item["your practice"] || ""; const risky = item["why it is risky"] || ""; // If recommendations is an array => join with bullet let recValue = item["recommendations"]; if (Array.isArray(recValue)) { recValue = recValue.join("\n- "); // produce bullet lines recValue = `- ${recValue}`.trim(); } else if (typeof recValue !== "string") { recValue = ""; } // If "your practice" is exactly "No risky practices identified", skip entire item if (practice === "No risky practices identified") { // do nothing: no outputFields[keyName], so we won't see "analysis_results 2" etc. return; } // Construct a single text block with line breaks let bigText = `Your Practice: ${practice}\n\n` + `Why It Is Risky: ${risky}\n\n` + `Recommendations:\n${recValue}\n\n`; // Store in outputFields outputFields[keyName] = bigText.trim(); }); } // Return the final object return outputFields; } catch (error) { return { error: "Invalid JSON or parse error", details: error.toString() }; }
This step saves the structured Responsible AI recommendations into Firestore, associating them with the respective user for future reference and report updates.
- In Zapier, add a new action.
- Select "Firebase / Firestore" as the action app.
- Choose "Create Cloud Firestore Document" as the action event.
- Click Continue to configure the Firestore step.
- Under Collection, set the value to:
Generated_Customized_Recommendations
- Set Convert Numerics to True.
- Under Document ID, set:
- Key:
Email - Value: Select the user’s email from the previous steps.
- Key:
- Under Data, map the sections to the formatted analysis results from Zap 16.

This step updates the AI-generated assessment report and compliance recommendations in Webflow, making the results accessible to users.
- In Zapier, add a new action.
- Select "Webflow" as the action app.
- Choose "Update Live Item" as the action event.
- Click Continue to configure Webflow.
- Configure your report contents and map the Required Fields:

- Click Continue to proceed.
- Click Test to verify Webflow updates with the correct data.
- Once successful, Publish the Zap to make it live.
This step updates the response status in Firebase Firestore to indicate that the recommendation process has been completed for a given user.
- In Zapier, add a new action.
- Select "Firebase / Firestore" as the action app.
- Choose "Create Cloud Firestore Document" as the action event.
- Click Continue to configure the Firestore settings.
- Under Setup Action, configure the following:
- Collection:
Response_Status - Convert Numerics: Set to
True - Document ID: Select the user's email (
Field Data Email) - Data Fields:
slug: Select Final Slug from the previous step.completed: Set this field totrueto mark completion.Item ID: Select the corresponding ID from Webflow.
- Collection:
- Click Continue, then Test & Review to ensure the Firestore record is updated.

- If successful, click Publish to finalize the step.
RADARs uses Inputflow to create dynamic survey flows and branching logic.
-
Install the Inputflow Plugin
- In the Webflow site, go to Apps.
- Search Inputflow and add it to your project.
- Alternatively, install from Inputflow’s official site.

-
Create a New Survey Flow
-
Set Up Branching Logic
-
Connect Inputflow Data to Firebase
- Ensure Webflow form submissions store responses in Webflow CMS.
- Use Zapier to send collected data to Firebase Firestore.
- Test by filling out a form and verifying data storage.
-
Final Check: Preview the survey, validate navigation between steps, and ensure responses trigger the right follow-up questions.
- The system uses OpenAI API (or Claude API) to analyze inputs.
- If you want to customize AI-generated reports:
- Modify API prompts in Zapier.
- Use a different AI model if needed.
-
Create a New CMS Collection
- In your Webflow Dashboard, open the project.
- Go to CMS → Add Collection.
- Name it (e.g., “AI Transparency Reports”).
- Add relevant fields (see “Example Fields” below).
-
Structuring Data
- Each CMS item can represent an entire report or a specific section.
- Keep fields clear and consistent for easy integration with the front end.
-
Connecting to External Tools
- If using Zapier or a custom API, grab your Webflow API Key from Project Settings.
- In Zapier, create steps that Create/Update CMS Item in Webflow whenever changes occur.
-
Best Practices
- Use descriptive field names (e.g.,
dataProvenance,modelTraining). - Make use of Rich Text fields if you need formatting (lists, bold text, etc.).
- Use descriptive field names (e.g.,
- Baseline Model: Short text about the AI’s base architecture
- Intended Use Cases: One-liner on primary scenarios (e.g., “clinical triage”)
- Industry: e.g., “Healthcare,” “Finance”
- Data Provenance: Where training data is sourced (public, private)
- Data Usage: Brief explanation of usage (e.g., training, feedback loops)
- Model Training: Key aspects of model training or fine-tuning
- Security: Approach to encryption, access control
- Fairness & Bias: High-level bias testing or balanced datasets
- SectionX Name/Score: Custom label and numeric risk levels (1–10)
- Risk Severity: Simple label (“Low,” “Medium,” “High”)
- Complete: Boolean to mark if the report is finished
-
Design a Template Page
- In the Webflow Designer, create a Page or Template that displays your CMS data.
- Use Collection Lists/Pages to bind fields (e.g., Baseline Model) to text blocks.
-
Bind Data Fields
- Drag a Collection List into the canvas.
- Connect it to your “AI Transparency Reports” CMS Collection.
- Map each field (e.g., Data Usage →
dataUsage) to a corresponding text or Rich Text element.
-
Style the Layout
- Adjust fonts, spacing, colors in the Style Panel.
- Add buttons for Edit or Download if needed.
-
Optional: Embed Custom Code
- For advanced interactivity, embed HTML/JS to manipulate the displayed data.
- Make sure your script references the correct CMS fields if you want dynamic updates.
-
Tips & Tricks
- Use staging environments to test changes.
- Preview content as different roles (e.g., editor, collaborator) to confirm access and visibility.
-
Editable vs. Display Containers
- Display Container: Shown by default, presents the current report section content.
- Edit Container: Hidden initially; contains input fields for users to modify the text.
- Toggle Logic: When the user clicks an “Edit” button:
- Display Container is hidden.
- Edit Container is shown, allowing the user to make changes.
- Upon clicking “Save,” the updates are sent (via Zapier) to a Firebase backend.
- The backend processes or stores these changes and forwards them back to the Webflow CMS.
- After the CMS is updated, the Display Container refreshes with the new content.
-
Edit Flow Example
- User clicks “Edit" Button
- Display Container is hidden; Edit Container is revealed with form fields populated by the current section text.
- User modifies text and clicks “Save.”
- Zapier sends updated content to Firebase (server-side).
- Firebase updates the relevant fields and triggers a CMS Update in Webflow.
- Page refreshes or re-renders to show the newly saved text in the Display Container again.
-
Advanced Tips
- If you need a WYSIWYG editor with more formatting options, consider integrating a tool like Quill or TipTap in a custom admin panel.
- Display a changelog or history to show who edited which sections and when.
Below is a breakdown of how this script enables editable sections in a Webflow page (or any webpage). It uses Zapier as an intermediary to handle data saving/updating. The script listens for user interactions on “Edit” and “Save” buttons, toggles visibility of containers, and sends updates to Zapier.
<script>
document.addEventListener("DOMContentLoaded", function() {
console.log("✅ Script Loaded: Webflow Editable Sections");
// Helper function to retrieve the current item ID
function getCurrentItemID() {
// 1) Prefer the email from localStorage if available
const userEmail = localStorage.getItem("userEmail");
if (userEmail) return userEmail;
// 2) Otherwise, as a fallback, use the final part of the URL path (slug)
const pathParts = window.location.pathname.split('/');
const slug = pathParts[pathParts.length - 1];
return slug || '';
}
// --- EDIT BUTTON EVENT LISTENER ---
document.querySelectorAll(".edit-button").forEach(button => {
button.addEventListener("click", function() {
// 1) Find the nearest container that wraps this section
let section = this.closest(".editable-section");
if (!section) return;
// 2) Retrieve the current item ID and store in a data attribute
const itemID = getCurrentItemID();
section.setAttribute('data-id', itemID);
console.log("✏️ Editing section:", section, "with ID:", itemID);
// 3) Hide the display container and show the edit container
section.querySelector(".display-container").style.display = "none";
section.querySelector(".edit-container").style.display = "block";
// 4) Populate the edit fields with existing text
section.querySelectorAll(".edit-field").forEach(input => {
let fieldKey = input.getAttribute("data-name")?.trim();
let displayField = section.querySelector(`.display-field[data-name='${fieldKey}']`);
if (displayField) {
input.value = displayField.innerText.trim();
}
});
});
});
// --- SAVE BUTTON EVENT LISTENER ---
document.querySelectorAll(".save-button").forEach(button => {
button.addEventListener("click", function() {
// 1) Identify the closest editable section
let section = this.closest(".editable-section");
if (!section) return;
// 2) Retrieve the item ID and the section type
const itemID = getCurrentItemID();
let sectionType = section.getAttribute("data-section")?.trim();
// 3) Build a data object to pass to Zapier
let updatedData = {
itemID: itemID,
userEmail: localStorage.getItem("userEmail"),
userName: localStorage.getItem("userName"),
userSlug: localStorage.getItem("userSlug")
};
// 4) Gather form data from all fields in the edit container
section.querySelectorAll(".edit-field").forEach(input => {
let fieldKey = input.getAttribute("data-name")?.trim();
if (fieldKey) {
updatedData[fieldKey] = input.value.trim();
}
});
// 5) Determine which Zapier webhook to use based on section type
const zapierURL = sectionType === "company"
? "https://hooks.zapier.com/hooks/catch/21445374/2agkb7d/"
: "https://hooks.zapier.com/hooks/catch/21445374/2w6gjtu/";
console.log("Sending data to Zapier:", updatedData);
// 6) POST the updatedData to Zapier
fetch(zapierURL, {
method: "POST",
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
mode: 'no-cors',
body: JSON.stringify(updatedData)
})
.then(() => {
console.log(`✅ Data sent successfully to ${sectionType} webhook`);
alert(`✅ Changes saved for ${sectionType}!`);
// 7) Update the display fields with the new text
section.querySelectorAll(".display-field").forEach(display => {
let fieldKey = display.getAttribute("data-name")?.trim();
if (updatedData[fieldKey]) {
display.innerText = updatedData[fieldKey];
}
});
// 8) Hide the edit container and show the display container again
section.querySelector(".edit-container").style.display = "none";
section.querySelector(".display-container").style.display = "block";
})
.catch(error => {
console.error("❌ Save Error:", error);
alert("⚠️ Failed to save. Please check console.");
});
});
});
});
</script>
This script is responsible for managing the display of risk cards (elements with the class .risk-card) and a success card (with the class .success-card). It examines each .risk-card to see if its .risk-number contains a non-empty and non-zero value. If a card’s .risk-number is 0 or empty, that card is hidden. Once the script has evaluated all cards, it checks whether any cards remain visible. If no risk cards are visible, it shows the .success-card; otherwise, it hides the success card.
-
Query for Cards & Success Card
The script locates all.risk-cardelements and a single.success-cardelement.
It logs how many.risk-cardelements it finds, helping you debug if they’re not being recognized. -
Check
.risk-number
Each card is expected to have a child element with.risk-number.
If.risk-numberis missing, that card is automatically hidden.
If.risk-numbertext is"0"or empty, the card is again hidden. -
Counting Visible Cards
ThevisibleCardscounter increments whenever a non-zero risk card is shown.
This allows the script to know if any risk remains. -
Showing the Success Card
IfvisibleCardsis0after checking all.risk-cardelements, the script concludes there are no visible risks.
It then sets the.success-cardtodisplay: block. If there’s at least one risk card visible,.success-cardis hidden instead. -
Use Cases
- Ideal for dashboards or reporting pages where you list potential risk categories.
- If all categories are at “0” or irrelevant, the user immediately sees a “No Risk” or “All Clear” message (the success card).
-
Styling
By default, the script uses inlined CSS (style.display = "none"or"block").
You can also add or remove classes for more advanced styling transitions. -
Placement
Ensure that.risk-cardand.success-cardelements exist in the DOM before this script is included.
In Webflow, you’d typically place this in the Footer Code or after the elements in a custom<embed>block. -
Debug Logs
The script includesconsole.log()statements to trace which elements are being hidden or shown.
Open your browser’s DevTools console to see these logs and diagnose any unexpected behavior.
Below is the full script you can place in your .md documentation or project README to explain how the functionality works and provide the exact code:
<script>
document.addEventListener("DOMContentLoaded", function() {
console.log("=== Debug: Script is running ===");
// 1. Get all risk-card elements & the success-card
const riskCards = document.querySelectorAll(".risk-card");
const successCard = document.querySelector(".success-card");
console.log("Found " + riskCards.length + " risk-cards.");
// Will track how many cards remain visible after processing
let visibleCards = 0;
// 2. Iterate over each .risk-card
riskCards.forEach((card, index) => {
// Locate the .risk-number within the card
const riskNumberEl = card.querySelector(".risk-number");
// If there's no .risk-number element, hide the card immediately
if (!riskNumberEl) {
console.log("Card #" + index + " has NO .risk-number element. Hiding card.");
card.style.display = "none";
return;
}
// Read the text content of .risk-number
const textContent = riskNumberEl.textContent.trim();
console.log("Card #" + index + " risk-number text:", `"${textContent}"`);
// If it's "0" or empty, hide the card; otherwise, show it
if (textContent === "0" || textContent === "") {
console.log("Card #" + index + " is 0 or empty, hiding.");
card.style.display = "none";
} else {
console.log("Card #" + index + " is NOT 0/empty, showing.");
card.style.display = "block";
visibleCards++;
}
});
// 3. If no cards are visible, show the success card; otherwise hide it
if (successCard) {
if (visibleCards === 0) {
successCard.style.display = "block";
console.log("No visible cards. Showing success-card.");
} else {
successCard.style.display = "none";
console.log("There is at least one visible card. Hiding success-card.");
}
}
});
</script>
<img width="1512" alt="Screenshot 2025-03-19 at 11 01 52" src="https://github.com/user-attachments/assets/620bb3ec-1054-4ccc-aa29-1144835b6f8f" />-
Approach 1: Third-Party PDF Tools
- Use a service like Print Friendly & PDF.
- Add a “Download as PDF” button that triggers the conversion of the visible page.
-
Approach 2: Custom PDF Generation
- On your server (Node.js, Python, etc.), fetch the CMS data, render an HTML template, and convert to PDF (using Puppeteer, pdfkit, etc.).
- Provide a download link to the user (
/download-report/:id).
-
Approach 3: In-Page Generation with html2canvas & jsPDF
- Embed the script below in your Webflow page (usually in the Before section under Project Settings or directly on the page’s custom code area).
- Create a button with
id="download-pdf". - Ensure you have container elements (e.g.,
container-1,container-2, etc.) that wrap each section you want captured in the PDF.
<!-- Include the libraries -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<style>
/* Optional: Remove white backgrounds to allow your gradient or image to show through. */
.no-white-bg {
background: transparent !important;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
const { jsPDF } = window.jspdf;
const downloadBtn = document.getElementById("download-pdf");
// List of container IDs that hold different parts of your report
const containerIds = ["container-1", "container-2", "container-3", "container-4", "container-5", "container-6"];
// 1) Your gradient or background image:
// Replace with a valid URL or Data URL to match your brand/theme.
// Example below is just a placeholder image from Webflow’s CDN.
const gradientImageDataURL = "https://cdn.prod.website-files.com/677dc834ff0ea40eb8b99402/67d339c7cfa63bb2e876da3b_image%20480.png";
downloadBtn.addEventListener("click", function () {
// 2) Create jsPDF for an A4 (portrait)
const pdf = new jsPDF("p", "mm", "a4");
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
// Set margins to control the content placement
const marginLeft = 10;
const marginTop = 10;
const marginBottom = 10;
const usableWidth = pdfWidth - marginLeft * 2;
const usableHeight = pdfHeight - marginTop - marginBottom;
let isFirstContainer = true;
// 3) Process each container in sequence
function processContainer(index) {
if (index >= containerIds.length) {
// Once all containers are processed, save the PDF
pdf.save("Responsible_AI_Disclosure_Report.pdf");
return;
}
const containerId = containerIds[index];
const containerEl = document.getElementById(containerId);
// If the container doesn't exist, skip to the next
if (!containerEl) {
processContainer(index + 1);
return;
}
// ──────────────────────────────────────────────────────────────────────
// EXAMPLE: Condition for container-2 (or any container with dynamic fields)
// If the user left some text area empty, hide it before capturing.
// ──────────────────────────────────────────────────────────────────────
if (containerId === "container-2") {
const mdaContainerEl = containerEl.querySelector(".display-container");
if (mdaContainerEl) {
const displayFieldEl = mdaContainerEl.querySelector(".display-field");
if (displayFieldEl) {
const textContent = displayFieldEl.textContent.trim();
if (!textContent) {
// Hide the entire sub-container if it's empty
mdaContainerEl.style.display = "none";
}
}
}
}
// 4) Capture the container with html2canvas
html2canvas(containerEl, {
scale: 1.5, // Increase for better quality, but note larger file sizes
backgroundColor: null,
ignoreElements: (element) => {
// Ignore buttons, "no-print" elements, etc.
if (element.tagName.toLowerCase() === 'button') return true;
if (element.classList && element.className.includes("no-print")) return true;
return false;
}
}).then((canvas) => {
// Convert canvas to image data
const imgData = canvas.toDataURL("image/png");
// Scale height to fit the PDF width
const imgHeight = (canvas.height * usableWidth) / canvas.width;
// 5) For all but the first container, add a new page
if (!isFirstContainer) {
pdf.addPage();
} else {
isFirstContainer = false;
}
// 6) Draw the background first
pdf.addImage(
gradientImageDataURL,
"PNG",
0, // x
0, // y
pdfWidth, // fill entire width
pdfHeight // fill entire height
);
// 7) Place the container screenshot
pdf.addImage(
imgData,
"PNG",
marginLeft,
marginTop,
usableWidth,
imgHeight
);
// 8) Move on to the next container
processContainer(index + 1);
});
}
// Start the recursive process
processContainer(0);
});
});
</script>









