--- title: Math Notation from 0 to 1: A Beginner's Guide description: Learn the fundamental mathematical notations that form the building blocks of mathematical communication, from basic symbols to calculus notation. tags: ['mathematics'] --- # Math Notation from 0 to 1: A Beginner's Guide ## TLDR Mathematical notation is a universal language that allows precise communication of complex ideas. This guide covers the essential math symbols and conventions you need to know, from basic arithmetic operations to more advanced calculus notation. You'll learn how to read and write mathematical expressions properly, understand the order of operations, and interpret common notations for sets, functions, and sequences. By mastering these fundamentals, you'll be better equipped to understand technical documentation, academic papers, and algorithms in computer science. ## Why Math Notation Matters Mathematical notation is like a universal language that allows precise communication of ideas. While it might seem intimidating at first, learning math notation will help you: - Understand textbooks and online resources more easily - Communicate mathematical ideas clearly - Solve problems more efficiently - Build a foundation for more advanced topics ## Basic Symbols ### Arithmetic Operations Let's start with the four basic operations: - Addition: $a + b$ - Subtraction: $a - b$ - Multiplication: $a \times b$ or $a \cdot b$ or simply $ab$ - Division: $a \div b$ or $\frac{a}{b}$ In more advanced mathematics, multiplication is often written without a symbol ($ab$ instead of $a \times b$) to save space and improve readability. ### Equality and Inequality - Equal to: $a = b$ - Not equal to: $a \neq b$ - Approximately equal to: $a \approx b$ - Less than: $a < b$ - Greater than: $a > b$ - Less than or equal to: $a \leq b$ - Greater than or equal to: $a \geq b$ ### Parentheses and Order of Operations Parentheses are used to show which operations should be performed first: $2 \times (3 + 4) = 2 \times 7 = 14$ Without parentheses, we follow the order of operations (often remembered with the acronym PEMDAS): - **P**arentheses - **E**xponents - **M**ultiplication and **D**ivision (from left to right) - **A**ddition and **S**ubtraction (from left to right) Example: $2 \times 3 + 4 = 6 + 4 = 10$ ## Exponents and Radicals ### Exponents (Powers) Exponents indicate repeated multiplication: $a^n = a \times a \times ... \times a$ (multiplied $n$ times) Examples: - $2^3 = 2 \times 2 \times 2 = 8$ - $10^2 = 10 \times 10 = 100$ ### Radicals (Roots) Radicals represent the inverse of exponents: $\sqrt[n]{a} = a^{1/n}$ Examples: - $\sqrt{9} = 3$ (because $3^2 = 9$) - $\sqrt[3]{8} = 2$ (because $2^3 = 8$) The square root ($\sqrt{}$) is the most common radical and means the same as $\sqrt[2]{}$. ## Vector Notation Vectors are quantities that have both magnitude and direction. They are commonly represented in several ways: ### Vector Representation - Bold letters: $\mathbf{v}$ or $\mathbf{a}$ - Arrow notation: $\vec{v}$ or $\vec{a}$ - Component form: $(v_1, v_2, v_3)$ for a 3D vector ### Vector Operations - Vector addition: $\mathbf{a} + \mathbf{b} = (a_1 + b_1, a_2 + b_2, a_3 + b_3)$ - Vector subtraction: $\mathbf{a} - \mathbf{b} = (a_1 - b_1, a_2 - b_2, a_3 - b_3)$ - Scalar multiplication: $c\mathbf{a} = (ca_1, ca_2, ca_3)$ ### Vector Products - Dot product (scalar product): $\mathbf{a} \cdot \mathbf{b} = a_1b_1 + a_2b_2 + a_3b_3$ - The dot product produces a scalar - If $\mathbf{a} \cdot \mathbf{b} = 0$, the vectors are perpendicular - Cross product (vector product): $\mathbf{a} \times \mathbf{b} = (a_2b_3 - a_3b_2, a_3b_1 - a_1b_3, a_1b_2 - a_2b_1)$ - The cross product produces a vector perpendicular to both $\mathbf{a}$ and $\mathbf{b}$ - Only defined for 3D vectors ### Vector Magnitude The magnitude or length of a vector $\mathbf{v} = (v_1, v_2, v_3)$ is: $|\mathbf{v}| = \sqrt{v_1^2 + v_2^2 + v_3^2}$ ### Unit Vectors A unit vector has a magnitude of 1 and preserves the direction of the original vector: $\hat{\mathbf{v}} = \frac{\mathbf{v}}{|\mathbf{v}|}$ Common unit vectors in the Cartesian coordinate system are: - $\hat{\mathbf{i}} = (1,0,0)$ (x-direction) - $\hat{\mathbf{j}} = (0,1,0)$ (y-direction) - $\hat{\mathbf{k}} = (0,0,1)$ (z-direction) Any vector can be written as: $\mathbf{v} = v_1\hat{\mathbf{i}} + v_2\hat{\mathbf{j}} + v_3\hat{\mathbf{k}}$ ## Fractions and Decimals ### Fractions A fraction represents division and consists of: - Numerator (top number) - Denominator (bottom number) $\frac{a}{b}$ means $a$ divided by $b$ Examples: - $\frac{1}{2} = 0.5$ - $\frac{3}{4} = 0.75$ ### Decimals and Percentages Decimals are another way to represent fractions: - $0.5 = \frac{5}{10} = \frac{1}{2}$ - $0.25 = \frac{25}{100} = \frac{1}{4}$ Percentages represent parts per hundred: - $50\% = \frac{50}{100} = 0.5$ - $25\% = \frac{25}{100} = 0.25$ ## Variables and Constants ### Variables Variables are symbols (usually letters) that represent unknown or changing values: - $x$, $y$, and $z$ are commonly used for unknown values - $t$ often represents time - $n$ often represents a count or integer ### Constants Constants are symbols that represent fixed, known values: - $\pi$ (pi) ≈ 3.14159... (the ratio of a circle's circumference to its diameter) - $e$ ≈ 2.71828... (the base of natural logarithms) - $i$ = $\sqrt{-1}$ (the imaginary unit) ## Functions A function relates an input to an output and is often written as $f(x)$, which is read as "f of x": $f(x) = x^2$ This means that the function $f$ takes an input $x$ and returns $x^2$. Examples: - If $f(x) = x^2$, then $f(3) = 3^2 = 9$ - If $g(x) = 2x + 1$, then $g(4) = 2 \times 4 + 1 = 9$ ## Sets and Logic ### Set Notation Sets are collections of objects, usually written with curly braces: - $\{1, 2, 3\}$ is the set containing the numbers 1, 2, and 3 - $\{x : x > 0\}$ is the set of all positive numbers (read as "the set of all $x$ such that $x$ is greater than 0") ### Set Operations - Union: $A \cup B$ (elements in either $A$ or $B$ or both) - Intersection: $A \cap B$ (elements in both $A$ and $B$) - Element of: $a \in A$ (element $a$ belongs to set $A$) - Not element of: $a \notin A$ (element $a$ does not belong to set $A$) - Subset: $A \subseteq B$ ($A$ is contained within $B$) ### Logic Symbols - And: $\land$ - Or: $\lor$ - Not: $\lnot$ - Implies: $\Rightarrow$ - If and only if: $\Leftrightarrow$ ## Summation and Product Notation ### Summation (Sigma Notation) The sigma notation represents the sum of a sequence: $\sum_{i=1}^{n} a_i = a_1 + a_2 + \ldots + a_n$ Example: $\sum_{i=1}^{4} i^2 = 1^2 + 2^2 + 3^2 + 4^2 = 1 + 4 + 9 + 16 = 30$ ### Product (Pi Notation) The pi notation represents the product of a sequence: $\prod_{i=1}^{n} a_i = a_1 \times a_2 \times \ldots \times a_n$ Example: $\prod_{i=1}^{4} i = 1 \times 2 \times 3 \times 4 = 24$ ## Calculus Notation ### Limits Limits describe the behavior of a function as its input approaches a particular value: $\lim_{x \to a} f(x) = L$ This is read as "the limit of $f(x)$ as $x$ approaches $a$ equals $L$." ### Derivatives Derivatives represent rates of change and can be written in several ways: $f'(x)$ or $\frac{d}{dx}f(x)$ or $\frac{df}{dx}$ ### Integrals Integrals represent area under curves and can be definite or indefinite: - Indefinite integral: $\int f(x) \, dx$ - Definite integral: $\int_{a}^{b} f(x) \, dx$ ## Conclusion Mathematical notation might seem like a foreign language at first, but with practice, it becomes second nature. This guide has covered the basics from 0 to 1, but there's always more to learn. As you continue your mathematical journey, you'll encounter new symbols and notations, each designed to communicate complex ideas efficiently. Remember, mathematics is about ideas, not just symbols. The notation is simply a tool to express these ideas clearly and precisely. Practice reading and writing in this language, and soon you'll find yourself thinking in mathematical terms! ## Practice Exercises 1. Write the following in mathematical notation: - The sum of $x$ and $y$, divided by their product - The square root of the sum of $a$ squared and $b$ squared - The set of all even numbers between 1 and 10 2. Interpret the following notations: - $f(x) = |x|$ - $\sum_{i=1}^{5} (2i - 1)$ - $\{x \in \mathbb{R} : -1 < x < 1\}$ Happy calculating! --- --- title: How to Implement a Cosine Similarity Function in TypeScript for Vector Comparison description: Learn how to build an efficient cosine similarity function in TypeScript for comparing vector embeddings. This step-by-step guide includes code examples, performance optimizations, and practical applications for semantic search and AI recommendation systems tags: ['typescript', 'ai', 'mathematics'] --- # How to Implement a Cosine Similarity Function in TypeScript for Vector Comparison To understand how an AI can understand that the word "cat" is similar to "kitten," you must realize cosine similarity. In short, with the help of embeddings, we can represent words as vectors in a high-dimensional space. If the word "cat" is represented as a vector [1, 0, 0], the word "kitten" would be represented as [1, 0, 1]. Now, we can use cosine similarity to measure the similarity between the two vectors. In this blog post, we will break down the concept of cosine similarity and implement it in TypeScript. I won't explain how embeddings work in this blog post, but only how to use them. ## What Is Cosine Similarity? A Simple Explanation The cosine similarity formula measures how similar two vectors are by examining the angle between them, not their sizes. Here's how it works in plain English: 1. **What it does**: It tells you if two vectors point in the same direction, opposite directions, or somewhere in between. 2. **The calculation**: - First, multiply the corresponding elements of both vectors and add these products together (the dot product) - Then, calculate how long each vector is (its magnitude) - Finally, divide the dot product by the product of the two magnitudes 3. **The result**: - If you get 1, the vectors point in exactly the same direction (perfectly similar) - If you get 0, the vectors stand perpendicular to each other (completely unrelated) - If you get -1, the vectors point in exactly opposite directions (perfectly dissimilar) - Any value in between indicates the degree of similarity 4. **Why it's useful**: - It ignores vector size and focuses only on direction - This means you can consider two things similar even if one is much "bigger" than the other - For example, a short document about cats and a long document about cats would show similarity, despite their different lengths 5. **In AI applications**: - We convert words, documents, images, etc. into vectors with many dimensions - Cosine similarity helps us find related items by measuring how closely their vectors align - This powers features like semantic search, recommendations, and content matching ## Why Cosine Similarity Matters for Modern Web Development When you build applications with any of these features, you directly work with vector mathematics: - **Semantic search**: Finding relevant content based on meaning, not just keywords - **AI-powered recommendations**: "Users who liked this also enjoyed..." - **Content matching**: Identifying similar articles, products, or user profiles - **Natural language processing**: Understanding and comparing text meaning All of these require you to compare vectors, and cosine similarity offers one of the most effective methods to do so. ## Visualizing Cosine Similarity ### Cosine Similarity Explained Cosine similarity measures the cosine of the angle between two vectors, showing how similar they are regardless of their magnitude. The value ranges from: - **+1**: When vectors point in the same direction (perfectly similar) - **0**: When vectors stand perpendicular (no similarity) - **-1**: When vectors point in opposite directions (completely dissimilar) With the interactive visualization above, you can: 1. Move both vectors by dragging the colored circles at their endpoints 2. Observe how the angle between them changes 3. See how cosine similarity relates to this angle 4. Note that cosine similarity depends only on the angle, not the vectors' lengths ## Step-by-Step Example Calculation Let me walk you through a manual calculation of cosine similarity between two simple vectors. This helps build intuition before we implement it in code. Given two vectors: $\vec{v_1} = [3, 4]$ and $\vec{v_2} = [5, 2]$ I'll calculate their cosine similarity step by step: **Step 1**: Calculate the dot product. $$ \vec{v_1} \cdot \vec{v_2} = 3 \times 5 + 4 \times 2 = 15 + 8 = 23 $$ **Step 2**: Calculate the magnitude of each vector. $$ ||\vec{v_1}|| = \sqrt{3^2 + 4^2} = \sqrt{9 + 16} = \sqrt{25} = 5 $$ $$ ||\vec{v_2}|| = \sqrt{5^2 + 2^2} = \sqrt{25 + 4} = \sqrt{29} \approx 5.385 $$ **Step 3**: Calculate the cosine similarity by dividing the dot product by the product of magnitudes. $$ \cos(\theta) = \frac{\vec{v_1} \cdot \vec{v_2}}{||\vec{v_1}|| \cdot ||\vec{v_2}||} $$ $$ = \frac{23}{5 \times 5.385} = \frac{23}{26.925} \approx 0.854 $$ Therefore, the cosine similarity between vectors $\vec{v_1}$ and $\vec{v_2}$ is approximately 0.854, which shows that these vectors point in roughly the same direction. ## Building a Cosine Similarity Function in TypeScript Let's implement an optimized cosine similarity function in TypeScript that combines the functional approach with the more efficient `Math.hypot()` method: ```typescript /** * Calculates the cosine similarity between two vectors * @param vecA First vector * @param vecB Second vector * @returns A value between -1 and 1, where 1 means identical */ function cosineSimilarity(vecA: number[], vecB: number[]): number { if (vecA.length !== vecB.length) { throw new Error("Vectors must have the same dimensions"); } // Calculate dot product: A·B = Σ(A[i] * B[i]) const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0); // Calculate magnitudes using Math.hypot() const magnitudeA = Math.hypot(...vecA); const magnitudeB = Math.hypot(...vecB); // Check for zero magnitude if (magnitudeA === 0 || magnitudeB === 0) { return 0; } // Calculate cosine similarity: (A·B) / (|A|*|B|) return dotProduct / (magnitudeA * magnitudeB); } ``` ## Testing Our Implementation Let's see how our function works with some example vectors: ```typescript // Example 1: Similar vectors pointing in roughly the same direction const vecA = [3, 4]; const vecB = [5, 2]; console.log(`Similarity: ${cosineSimilarity(vecA, vecB).toFixed(3)}`); // Output: Similarity: 0.857 // Example 2: Perpendicular vectors const vecC = [1, 0]; const vecD = [0, 1]; console.log(`Similarity: ${cosineSimilarity(vecC, vecD).toFixed(3)}`); // Output: Similarity: 0.000 // Example 3: Opposite vectors const vecE = [2, 3]; const vecF = [-2, -3]; console.log(`Similarity: ${cosineSimilarity(vecE, vecF).toFixed(3)}`); // Output: Similarity: -1.000 ``` Mathematically, we can verify these results: For Example 1: $$\text{cosine similarity} = \frac{3 \times 5 + 4 \times 2}{\sqrt{3^2 + 4^2} \times \sqrt{5^2 + 2^2}} = \frac{15 + 8}{\sqrt{25} \times \sqrt{29}} = \frac{23}{5 \times \sqrt{29}} \approx 0.857$$ For Example 2: $$\text{cosine similarity} = \frac{1 \times 0 + 0 \times 1}{\sqrt{1^2 + 0^2} \times \sqrt{0^2 + 1^2}} = \frac{0}{1 \times 1} = 0$$ For Example 3: $$\text{cosine similarity} = \frac{2 \times (-2) + 3 \times (-3)}{\sqrt{2^2 + 3^2} \times \sqrt{(-2)^2 + (-3)^2}} = \frac{-4 - 9}{\sqrt{13} \times \sqrt{13}} = \frac{-13}{13} = -1$$ ## Complete TypeScript Solution Here's a complete TypeScript solution that includes our cosine similarity function along with some utility methods: ```typescript class VectorUtils { /** * Calculates the cosine similarity between two vectors */ static cosineSimilarity(vecA: number[], vecB: number[]): number { if (vecA.length !== vecB.length) { throw new Error(`Vector dimensions don't match: ${vecA.length} vs ${vecB.length}`); } const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0); const magnitudeA = Math.hypot(...vecA); const magnitudeB = Math.hypot(...vecB); if (magnitudeA === 0 || magnitudeB === 0) { return 0; } return dotProduct / (magnitudeA * magnitudeB); } /** * Calculates the dot product of two vectors */ static dotProduct(vecA: number[], vecB: number[]): number { if (vecA.length !== vecB.length) { throw new Error(`Vector dimensions don't match: ${vecA.length} vs ${vecB.length}`); } return vecA.reduce((sum, a, i) => sum + a * vecB[i], 0); } /** * Calculates the magnitude (length) of a vector */ static magnitude(vec: number[]): number { return Math.hypot(...vec); } /** * Normalizes a vector (converts to unit vector) */ static normalize(vec: number[]): number[] { const mag = this.magnitude(vec); if (mag === 0) { return Array(vec.length).fill(0); } return vec.map(v => v / mag); } /** * Converts cosine similarity to angular distance in degrees */ static similarityToDegrees(similarity: number): number { // Clamp similarity to [-1, 1] to handle floating point errors const clampedSimilarity = Math.max(-1, Math.min(1, similarity)); return Math.acos(clampedSimilarity) * (180 / Math.PI); } } ``` ## Using Cosine Similarity in Real Web Applications When you work with AI in web applications, you'll often need to calculate similarity between vectors. Here's a practical example: ```typescript // Example: Semantic search implementation function semanticSearch(queryEmbedding: number[], documentEmbeddings: DocumentWithEmbedding[]): SearchResult[] { return documentEmbeddings .map(doc => ({ document: doc, relevance: VectorUtils.cosineSimilarity(queryEmbedding, doc.embedding) })) .filter(result => result.relevance > 0.7) // Only consider relevant results .sort((a, b) => b.relevance - a.relevance); } ``` ## Using OpenAI Embedding Models with Cosine Similarity While the examples above used simple vectors for clarity, real-world AI applications typically use embedding models that transform text and other data into high-dimensional vector spaces. OpenAI provides powerful embedding models that you can easily incorporate into your applications. These models transform text into vectors with hundreds or thousands of dimensions that capture semantic meaning: ```typescript // Example of using OpenAI embeddings with our cosine similarity function async function compareTextSimilarity(textA: string, textB: string): Promise { // Get embeddings from OpenAI API const responseA = await fetch('https://api.openai.com/v1/embeddings', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'text-embedding-3-large', input: textA }) }); const responseB = await fetch('https://api.openai.com/v1/embeddings', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'text-embedding-3-large', input: textB }) }); const embeddingA = (await responseA.json()).data[0].embedding; const embeddingB = (await responseB.json()).data[0].embedding; // Calculate similarity using our function return VectorUtils.cosineSimilarity(embeddingA, embeddingB); } ``` In a production environment, you should pre-compute embeddings for your content (like blog posts, products, or documents) and store them in a vector database (like Pinecone, Qdrant, or Milvus). Re-computing embeddings for every user request as shown in this example wastes resources and slows performance. A better approach: embed your content once during indexing, store the vectors, and only embed the user's query when performing a search. OpenAI's latest embedding models like `text-embedding-3-large` have up to 3,072 dimensions, capturing extremely nuanced semantic relationships between words and concepts. These high-dimensional embeddings enable much more accurate similarity measurements than simpler vector representations. For more information on OpenAI's embedding models, including best practices and implementation details, check out their documentation at [https://platform.openai.com/docs/guides/embeddings](https://platform.openai.com/docs/guides/embeddings). ## Conclusion Understanding vectors and cosine similarity provides practical tools that empower you to work effectively with modern AI features. By implementing these concepts in TypeScript, you gain a deeper understanding and precise control over calculating similarity in your applications. The interactive visualizations we've explored help you build intuition about these mathematical concepts, while the TypeScript implementation gives you the tools to apply them in real-world scenarios. Whether you build recommendation systems, semantic search, or content-matching features, the foundation you've gained here will help you implement more intelligent, accurate, and effective AI-powered features in your web applications. --- --- title: How I Added llms.txt to My Astro Blog description: I built a simple way to load my blog content into any LLM with one click. This post shows how you can do it too. tags: ['astro', 'ai'] --- # How I Added llms.txt to My Astro Blog ## TLDR I created an endpoint in my Astro blog that outputs all posts in plain text format. This lets me copy my entire blog with one click and paste it into any LLM with adequate context window. The setup uses TypeScript and Astro's API routes, making it work with any Astro content collection. ## Why I Built This I wanted a quick way to ask AI models questions about my own blog content. Copying posts one by one is slow. With this solution, I can give any LLM all my blog posts at once. ## How It Works The solution creates a special endpoint that: 1. Gets all blog posts 2. Converts them to plain text 3. Formats them with basic metadata 4. Outputs everything as one big text file ## Setting Up the File First, I created a new TypeScript file in my Astro pages directory: ```ts // src/pages/llms.txt.ts // Function to extract the frontmatter as text const extractFrontmatter = (content: string): string => { const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); return frontmatterMatch ? frontmatterMatch[1] : ''; }; // Function to clean content while keeping frontmatter const cleanContent = (content: string): string => { // Extract the frontmatter as text const frontmatterText = extractFrontmatter(content); // Remove the frontmatter delimiters let cleanedContent = content.replace(/^---\n[\s\S]*?\n---/, ''); // Clean up MDX-specific imports cleanedContent = cleanedContent.replace(/import\s+.*\s+from\s+['"].*['"];?\s*/g, ''); // Remove MDX component declarations cleanedContent = cleanedContent.replace(/<\w+\s+.*?\/>/g, ''); // Remove Shiki Twoslash syntax like cleanedContent = cleanedContent.replace(/\/\/\s*@noErrors/g, ''); cleanedContent = cleanedContent.replace(/\/\/\s*@(.*?)$/gm, ''); // Remove other Shiki Twoslash directives // Clean up multiple newlines cleanedContent = cleanedContent.replace(/\n\s*\n\s*\n/g, '\n\n'); // Return the frontmatter as text, followed by the cleaned content return frontmatterText + '\n\n' + cleanedContent.trim(); }; export const GET: APIRoute = async () => { try { // Get all blog posts sorted by date (newest first) const posts = await getCollection('blog', ({ data }) => !data.draft); const sortedPosts = posts.sort((a, b) => new Date(b.data.pubDatetime).valueOf() - new Date(a.data.pubDatetime).valueOf() ); // Generate the content let llmsContent = ''; for (const post of sortedPosts) { // Add post metadata in the format similar to the example llmsContent += `--- title: ${post.data.title} description: ${post.data.description} tags: [${post.data.tags.map(tag => `'${tag}'`).join(', ')}] ---\n\n`; // Add the post title as a heading llmsContent += `# ${post.data.title}\n\n`; // Process the content, keeping frontmatter as text const processedContent = cleanContent(post.body); llmsContent += processedContent + '\n\n'; // Add separator between posts llmsContent += '---\n\n'; } // Return the response as plain text return new Response(llmsContent, { headers: { "Content-Type": "text/plain; charset=utf-8" }, }); } catch (error) { console.error('Failed to generate llms.txt:', error); return new Response('Error generating llms.txt', { status: 500 }); } }; ``` This code accomplishes four key functions: 1. It uses Astro's `getCollection` function to grab all published blog posts 2. It sorts them by date with newest first 3. It cleans up each post's content with helper functions 4. It formats each post with its metadata and content 5. It returns everything as plain text ## How to Use It Using this is simple: 1. Visit `alexop.dev/llms.txt` in your browser 2. Press Ctrl+A (or Cmd+A on Mac) to select all the text 3. Copy it (Ctrl+C or Cmd+C) 4. Paste it into any LLM with adequate context window (like ChatGPT, Claude, Llama, etc.) 5. Ask questions about your blog content The LLM now has all your blog posts in its context window. You can ask questions such as: - "What topics have I written about?" - "Summarize my post about [topic]" - "Find code examples in my posts that use [technology]" - "What have I written about [specific topic]?" ## Benefits of This Approach This approach offers distinct advantages: - Works with any Astro blog - Requires a single file to set up - Makes your content easy to query with any LLM - Keeps useful metadata with each post - Formats content in a way LLMs understand well ## Conclusion By adding one straightforward TypeScript file to your Astro blog, you can create a fast way to chat with your own content using any LLM with adequate context window. This makes it easy to: - Find information in your old posts - Get summaries of your content - Find patterns across your writing - Generate new ideas based on your past content Give it a try! The setup takes minutes, and it makes interacting with your blog content much faster. --- --- title: How to Do Visual Regression Testing in Vue with Vitest? description: Learn how to implement visual regression testing in Vue.js using Vitest's browser mode. This comprehensive guide covers setting up screenshot-based testing, creating component stories, and integrating with CI/CD pipelines for automated visual testing. tags: ['vue', 'testing', 'vitest'] --- # How to Do Visual Regression Testing in Vue with Vitest? TL;DR: Visual regression testing detects unintended UI changes by comparing screenshots. With Vitest's experimental browser mode and Playwright, you can: - **Run tests in a real browser environment** - **Define component stories for different states** - **Capture screenshots and compare them with baseline images using snapshot testing** In this guide, you'll learn how to set up visual regression testing for Vue components using Vitest. Our test will generate this screenshot: Visual regression testing captures screenshots of UI components and compares them against baseline images to flag visual discrepancies. This ensures consistent styling and layout across your design system. ## Vitest Configuration Start by configuring Vitest with the Vue plugin: ```typescript export default defineConfig({ plugins: [vue()], }) ``` ## Setting Up Browser Testing Visual regression tests need a real browser environment. Install these dependencies: ```bash npm install -D vitest @vitest/browser playwright ``` You can also use the following command to initialize the browser mode: ```bash npx vitest init browser ``` First, configure Vitest to support both unit and browser tests using a workspace file, `vitest.workspace.ts`. For more details on workspace configuration, see the [Vitest Workspace Documentation](https://vitest.dev/guide/workspace.html). Using a workspace configuration allows you to maintain separate settings for unit and browser tests while sharing common configuration. This makes it easier to manage different testing environments in your project. ```typescript export default defineWorkspace([ { extends: './vitest.config.ts', test: { name: 'unit', include: ['**/*.spec.ts', '**/*.spec.tsx'], exclude: ['**/*.browser.spec.ts', '**/*.browser.spec.tsx'], environment: 'jsdom', }, }, { extends: './vitest.config.ts', test: { name: 'browser', include: ['**/*.browser.spec.ts', '**/*.browser.spec.tsx'], browser: { enabled: true, provider: 'playwright', headless: true, instances: [{ browser: 'chromium' }], }, }, }, ]) ``` Add scripts in your `package.json` ```json { "scripts": { "test": "vitest", "test:unit": "vitest --project unit", "test:browser": "vitest --project browser" } } ``` Now we can run tests in separate environments like this: ```bash npm run test:unit npm run test:browser ``` ## The BaseButton Component Consider the `BaseButton.vue` component a reusable button with customizable size, variant, and disabled state: ```vue ``` ## Defining Stories for Testing Create "stories" to showcase different button configurations: ```typescript const buttonStories = [ { name: 'Primary Medium', props: { variant: 'primary', size: 'medium' }, slots: { default: 'Primary Button' }, }, { name: 'Secondary Medium', props: { variant: 'secondary', size: 'medium' }, slots: { default: 'Secondary Button' }, }, // and much more ... ] ``` Each story defines a name, props, and slot content. ## Rendering Stories for Screenshots Render all stories in one container to capture a comprehensive screenshot: ```typescript interface Story { name: string props: Record slots: Record } function renderStories(component: Component, stories: Story[]): HTMLElement { const container = document.createElement('div') container.style.display = 'flex' container.style.flexDirection = 'column' container.style.gap = '16px' container.style.padding = '20px' container.style.backgroundColor = '#ffffff' stories.forEach((story) => { const storyWrapper = document.createElement('div') const label = document.createElement('h3') label.textContent = story.name storyWrapper.appendChild(label) const { container: storyContainer } = render(component, { props: story.props, slots: story.slots, }) storyWrapper.appendChild(storyContainer) container.appendChild(storyWrapper) }) return container } ``` ## Writing the Visual Regression Test Write a test that renders the stories and captures a screenshot: ```typescript // [buttonStories and renderStories defined above] describe('BaseButton', () => { describe('visual regression', () => { it('should match all button variants snapshot', async () => { const container = renderStories(BaseButton, buttonStories) document.body.appendChild(container) const screenshot = await page.screenshot({ path: 'all-button-variants.png', }) // this assertion is acutaly not doing anything // but otherwise you would get a warning about the screenshot not being taken expect(screenshot).toBeTruthy() document.body.removeChild(container) }) }) }) ``` Use `render` from `vitest-browser-vue` to capture components as they appear in a real browser. Save this file with a `.browser.spec.ts` extension (e.g., `BaseButton.browser.spec.ts`) to match your browser test configuration. ## Beyond Screenshots: Automated Comparison Automate image comparison by encoding screenshots in base64 and comparing them against baseline snapshots: ```typescript // Helper function to take and compare screenshots async function takeAndCompareScreenshot(name: string, element: HTMLElement) { const screenshotDir = './__screenshots__' const snapshotDir = './__snapshots__' const screenshotPath = `${screenshotDir}/${name}.png` // Append element to body document.body.appendChild(element) // Take screenshot const screenshot = await page.screenshot({ path: screenshotPath, base64: true, }) // Compare base64 snapshot await expect(screenshot.base64).toMatchFileSnapshot(`${snapshotDir}/${name}.snap`) // Save PNG for reference await expect(screenshot.path).toBeTruthy() // Cleanup document.body.removeChild(element) } ``` Then update the test: ```typescript describe('BaseButton', () => { describe('visual regression', () => { it('should match all button variants snapshot', async () => { const container = renderStories(BaseButton, buttonStories) await expect( takeAndCompareScreenshot('all-button-variants', container) ).resolves.not.toThrow() }) }) }) ``` Vitest is discussing native screenshot comparisons in browser mode. Follow and contribute at [github.com/vitest-dev/vitest/discussions/690](https://github.com/vitest-dev/vitest/discussions/690). ```mermaid flowchart LR A[Render Component] --> B[Capture Screenshot] B --> C{Compare with Baseline} C -->|Match| D[Test Passes] C -->|Difference| E[Review Changes] E -->|Accept| F[Update Baseline] E -->|Reject| G[Fix Component] G --> A ``` ## Conclusion Vitest's experimental browser mode empowers developers to perform accurate visual regression testing of Vue components in real browser environments. While the current workflow requires manual review of screenshot comparisons, it establishes a foundation for more automated visual testing in the future. This approach also strengthens collaboration between developers and UI designers. Designers can review visual changes to components before production deployment by accessing the generated screenshots in the component library. For advanced visual testing capabilities, teams should explore dedicated tools like Playwright or Cypress that offer more features and maturity. Keep in mind to perform visual regression tests against your Base components. --- --- title: How to Test Vue Router Components with Testing Library and Vitest description: Learn how to test Vue Router components using Testing Library and Vitest. This guide covers real router integration, mocked router setups, and best practices for testing navigation, route guards, and dynamic components in Vue applications. tags: ['vue', 'testing', 'vue-router', 'vitest', 'testing-library'] --- # How to Test Vue Router Components with Testing Library and Vitest ## TLDR This guide shows you how to test Vue Router components using real router integration and isolated component testing with mocks. You'll learn to verify router-link interactions, programmatic navigation, and navigation guard handling. ## Introduction Modern Vue applications need thorough testing to ensure reliable navigation and component performance. We'll cover testing strategies using Testing Library and Vitest to simulate real-world scenarios through router integration and component isolation. ## Vue Router Testing Techniques with Testing Library and Vitest Let's explore how to write effective tests for Vue Router components using both real router instances and mocks. ## Testing Vue Router Navigation Components ### Navigation Component Example ```vue ``` ### Real Router Integration Testing Test complete routing behavior with a real router instance: ```typescript describe('NavigationMenu', () => { it('should navigate using router links', async () => { const router = createRouter({ history: createWebHistory(), routes: [ { path: '/dashboard', component: { template: 'Dashboard' } }, { path: '/settings', component: { template: 'Settings' } }, { path: '/profile', component: { template: 'Profile' } }, { path: '/', component: { template: 'Home' } }, ], }) render(NavigationMenu, { global: { plugins: [router], }, }) const user = userEvent.setup() expect(router.currentRoute.value.path).toBe('/') await router.isReady() await user.click(screen.getByText('Dashboard')) expect(router.currentRoute.value.path).toBe('/dashboard') await user.click(screen.getByText('Profile')) expect(router.currentRoute.value.path).toBe('/profile') }) }) ``` ### Mocked Router Testing Test components in isolation with router mocks: ```typescript const mockPush = vi.fn() vi.mock('vue-router', () => ({ useRouter: vi.fn(), })) describe('NavigationMenu with mocked router', () => { it('should handle navigation with mocked router', async () => { const mockRouter = { push: mockPush, currentRoute: { value: { path: '/' } }, } as unknown as Router vi.mocked(useRouter).mockImplementation(() => mockRouter) const user = userEvent.setup() render(NavigationMenu) await user.click(screen.getByText('Profile')) expect(mockPush).toHaveBeenCalledWith('/profile') }) }) ``` ### RouterLink Stub for Isolated Testing Create a RouterLink stub to test navigation without router-link behavior: ```ts // test-utils.ts export const RouterLinkStub: Component = { name: 'RouterLinkStub', props: { to: { type: [String, Object], required: true, }, tag: { type: String, default: 'a', }, exact: Boolean, exactPath: Boolean, append: Boolean, replace: Boolean, activeClass: String, exactActiveClass: String, exactPathActiveClass: String, event: { type: [String, Array], default: 'click', }, }, setup(props) { const router = useRouter() const navigate = () => { router.push(props.to) } return { navigate } }, render() { return h( this.tag, { onClick: () => this.navigate(), }, this.$slots.default?.(), ) }, } ``` Use the RouterLinkStub in tests: ```ts const mockPush = vi.fn() vi.mock('vue-router', () => ({ useRouter: vi.fn(), })) describe('NavigationMenu with mocked router', () => { it('should handle navigation with mocked router', async () => { const mockRouter = { push: mockPush, currentRoute: { value: { path: '/' } }, } as unknown as Router vi.mocked(useRouter).mockImplementation(() => mockRouter) const user = userEvent.setup() render(NavigationMenu, { global: { stubs: { RouterLink: RouterLinkStub, }, }, }) await user.click(screen.getByText('Dashboard')) expect(mockPush).toHaveBeenCalledWith('/dashboard') }) }) ``` ### Testing Navigation Guards Test navigation guards by rendering the component within a route context: ```vue ``` Test the navigation guard: ```ts const routes = [ { path: '/', component: RouteLeaveGuardDemo }, { path: '/about', component: { template: '
About
' } }, ] const router = createRouter({ history: createWebHistory(), routes, }) const App = { template: '' } describe('RouteLeaveGuardDemo', () => { beforeEach(async () => { vi.clearAllMocks() window.confirm = vi.fn() await router.push('/') await router.isReady() }) it('should prompt when guard is triggered and user confirms', async () => { // Set window.confirm to simulate a user confirming the prompt window.confirm = vi.fn(() => true) // Render the component within a router context render(App, { global: { plugins: [router], }, }) const user = userEvent.setup() // Find the 'About' link and simulate a user click const aboutLink = screen.getByRole('link', { name: /About/i }) await user.click(aboutLink) // Assert that the confirm dialog was shown with the correct message expect(window.confirm).toHaveBeenCalledWith('Do you really want to leave this page?') // Verify that the navigation was allowed and the route changed to '/about' expect(router.currentRoute.value.path).toBe('/about') }) }) ``` ### Reusable Router Test Helper Create a helper function to simplify router setup: ```typescript // test-utils.ts // path of the definition of your routes interface RenderWithRouterOptions extends Omit, 'global'> { initialRoute?: string routerOptions?: { routes?: typeof routes history?: ReturnType } } export function renderWithRouter(Component: any, options: RenderWithRouterOptions = {}) { const { initialRoute = '/', routerOptions = {}, ...renderOptions } = options const router = createRouter({ history: createWebHistory(), // Use provided routes or import from your router file routes: routerOptions.routes || routes, }) router.push(initialRoute) return { // Return everything from regular render, plus the router instance ...render(Component, { global: { plugins: [router], }, ...renderOptions, }), router, } } ``` Use the helper in tests: ```typescript describe('NavigationMenu', () => { it('should navigate using router links', async () => { const { router } = renderWithRouter(NavigationMenu, { initialRoute: '/', }) await router.isReady() const user = userEvent.setup() await user.click(screen.getByText('Dashboard')) expect(router.currentRoute.value.path).toBe('/dashboard') }) }) ``` ### Conclusion: Best Practices for Vue Router Component Testing When we test components that rely on the router, we need to consider whether we want to test the functionality in the most realistic use case or in isolation. In my humble opinion, the more you mock a test, the worse it will get. My personal advice would be to aim to use the real router instead of mocking it. Sometimes, there are exceptions, so keep that in mind. Also, you can help yourself by focusing on components that don't rely on router functionality. Reserve router logic for view/page components. While keeping our components simple, we will never have the problem of mocking the router in the first place. --- --- title: How to Use AI for Effective Diagram Creation: A Guide to ChatGPT and Mermaid description: Learn how to leverage ChatGPT and Mermaid to create effective diagrams for technical documentation and communication. tags: ['ai', 'productivity'] --- # How to Use AI for Effective Diagram Creation: A Guide to ChatGPT and Mermaid ## TLDR Learn how to combine ChatGPT and Mermaid to quickly create professional diagrams for technical documentation. This approach eliminates the complexity of traditional diagramming tools while maintaining high-quality output. ## Introduction Mermaid is a markdown-like script language that generates diagrams from text descriptions. When combined with ChatGPT, it becomes a powerful tool for creating technical diagrams quickly and efficiently. ## Key Diagram Types ### Flowcharts Perfect for visualizing processes: ```plaintext flowchart LR A[Customer selects products] --> B[Customer reviews order] B --> C{Payment Successful?} C -->|Yes| D[Generate Invoice] D --> E[Dispatch goods] C -->|No| F[Redirect to Payment] ``` ```mermaid flowchart LR A[Customer selects products] --> B[Customer reviews order] B --> C{Payment Successful?} C -->|Yes| D[Generate Invoice] D --> E[Dispatch goods] C -->|No| F[Redirect to Payment] ``` ### Sequence Diagrams Ideal for system interactions: ```plaintext sequenceDiagram participant Client participant Server Client->>Server: Request (GET /resource) Server-->>Client: Response (200 OK) ``` ```mermaid sequenceDiagram participant Client participant Server Client->>Server: Request (GET /resource) Server-->>Client: Response (200 OK) ``` ## Using ChatGPT with Mermaid 1. Ask ChatGPT to explain your concept 2. Request a Mermaid diagram representation 3. Iterate on the diagram with follow-up questions Example prompt: "Create a Mermaid sequence diagram showing how Nuxt.js performs server-side rendering" ```plaintext sequenceDiagram participant Client as Client Browser participant Nuxt as Nuxt.js Server participant Vue as Vue.js Application participant API as Backend API Client->>Nuxt: Initial Request Nuxt->>Vue: SSR Starts Vue->>API: API Calls (if any) API-->>Vue: API Responses Vue->>Nuxt: Rendered HTML Nuxt-->>Client: HTML Content ``` ```mermaid sequenceDiagram participant Client as Client Browser participant Nuxt as Nuxt.js Server participant Vue as Vue.js Application participant API as Backend API Client->>Nuxt: Initial Request Nuxt->>Vue: SSR Starts Vue->>API: API Calls (if any) API-->>Vue: API Responses Vue->>Nuxt: Rendered HTML Nuxt-->>Client: HTML Content ``` ## Quick Setup Guide ### Online Editor Use [Mermaid Live Editor](https://mermaid.live/) for quick prototyping. ### VS Code Integration 1. Install "Markdown Preview Mermaid Support" extension 2. Create `.md` file with Mermaid code blocks 3. Preview with built-in markdown viewer ### Web Integration ```html
graph TD A-->B
``` ## Conclusion The combination of ChatGPT and Mermaid streamlines technical diagramming, making it accessible and efficient. Try it in your next documentation project to save time while creating professional diagrams. --- --- title: Building a Pinia Plugin for Cross-Tab State Syncing description: Learn how to create a Pinia plugin that synchronizes state across browser tabs using the BroadcastChannel API and Vue 3's Script Setup syntax. tags: ['vue', 'pinia'] --- # Building a Pinia Plugin for Cross-Tab State Syncing ## TLDR Create a Pinia plugin that enables state synchronization across browser tabs using the BroadcastChannel API. The plugin allows you to mark specific stores for cross-tab syncing and handles state updates automatically with timestamp-based conflict resolution. ## Introduction In modern web applications, users often work with multiple browser tabs open. When using Pinia for state management, we sometimes need to ensure that state changes in one tab are reflected across all open instances of our application. This post will guide you through creating a plugin that adds cross-tab state synchronization to your Pinia stores. ## Understanding Pinia Plugins A Pinia plugin is a function that extends the functionality of Pinia stores. Plugins are powerful tools that help: - Reduce code duplication - Add reusable functionality across stores - Keep store definitions clean and focused - Implement cross-cutting concerns ## Cross-Tab Communication with BroadcastChannel The BroadcastChannel API provides a simple way to send messages between different browser contexts (tabs, windows, or iframes) of the same origin. It's perfect for our use case of synchronizing state across tabs. Key features of BroadcastChannel: - Built-in browser API - Same-origin security model - Simple pub/sub messaging pattern - No need for external dependencies ### How BroadcastChannel Works The BroadcastChannel API operates on a simple principle: any browsing context (window, tab, iframe, or worker) can join a channel by creating a `BroadcastChannel` object with the same channel name. Once joined: 1. Messages are sent using the `postMessage()` method 2. Messages are received through the `onmessage` event handler 3. Contexts can leave the channel using the `close()` method ## Implementing the Plugin ### Store Configuration To use our plugin, stores need to opt-in to state sharing through configuration: ```ts twoslash export const useCounterStore = defineStore( 'counter', () => { const count = ref(0) const doubleCount = computed(() => count.value * 2) function increment() { count.value++ } return { count, doubleCount, increment } }, { share: { enable: true, initialize: true, }, }, ) ``` The `share` option enables cross-tab synchronization and controls whether the store should initialize its state from other tabs. ### Plugin Registration `main.ts` Register the plugin when creating your Pinia instance: ```ts twoslash const pinia = createPinia() pinia.use(PiniaSharedState) ``` ### Plugin Implementation `plugin/plugin.ts` Here's our complete plugin implementation with TypeScript support: ```ts twoslash type Serializer = { serialize: (value: T) => string deserialize: (value: string) => T } interface BroadcastMessage { type: 'STATE_UPDATE' | 'SYNC_REQUEST' timestamp?: number state?: string } type PluginOptions = { enable?: boolean initialize?: boolean serializer?: Serializer } export interface StoreOptions extends DefineStoreOptions { share?: PluginOptions } // Add type extension for Pinia declare module 'pinia' { // eslint-disable-next-line @typescript-eslint/no-unused-vars export interface DefineStoreOptionsBase { share?: PluginOptions } } export function PiniaSharedState({ enable = false, initialize = false, serializer = { serialize: JSON.stringify, deserialize: JSON.parse, }, }: PluginOptions = {}) { return ({ store, options }: PiniaPluginContext) => { if (!(options.share?.enable ?? enable)) return const channel = new BroadcastChannel(store.$id) let timestamp = 0 let externalUpdate = false // Initial state sync if (options.share?.initialize ?? initialize) { channel.postMessage({ type: 'SYNC_REQUEST' }) } // State change listener store.$subscribe((_mutation, state) => { if (externalUpdate) return timestamp = Date.now() channel.postMessage({ type: 'STATE_UPDATE', timestamp, state: serializer.serialize(state as T), }) }) // Message handler channel.onmessage = (event: MessageEvent) => { const data = event.data if ( data.type === 'STATE_UPDATE' && data.timestamp && data.timestamp > timestamp && data.state ) { externalUpdate = true timestamp = data.timestamp store.$patch(serializer.deserialize(data.state)) externalUpdate = false } if (data.type === 'SYNC_REQUEST') { channel.postMessage({ type: 'STATE_UPDATE', timestamp, state: serializer.serialize(store.$state as T), }) } } } } ``` The plugin works by: 1. Creating a BroadcastChannel for each store 2. Subscribing to store changes and broadcasting updates 3. Handling incoming messages from other tabs 4. Using timestamps to prevent update cycles 5. Supporting custom serialization for complex state ### Communication Flow Diagram ```mermaid flowchart LR A[User interacts with store in Tab 1] --> B[Store state changes] B --> C[Plugin detects change] C --> D[BroadcastChannel posts STATE_UPDATE] D --> E[Other tabs receive STATE_UPDATE] E --> F[Plugin patches store state in Tab 2] ``` ## Using the Synchronized Store Components can use the synchronized store just like any other Pinia store: ```ts twoslash const counterStore = useCounterStore() // State changes will automatically sync across tabs counterStore.increment() ``` ## Conclusion With this Pinia plugin, we've added cross-tab state synchronization with minimal configuration. The solution is lightweight, type-safe, and leverages the built-in BroadcastChannel API. This pattern is particularly useful for applications where users frequently work across multiple tabs and need a consistent state experience. Remember to consider the following when using this plugin: - Only enable sharing for stores that truly need it - Be mindful of performance with large state objects - Consider custom serialization for complex data structures - Test thoroughly across different browser scenarios ## Future Optimization: Web Workers For applications with heavy cross-tab communication or complex state transformations, consider offloading the BroadcastChannel handling to a Web Worker. This approach can improve performance by: - Moving message processing off the main thread - Handling complex state transformations without blocking UI - Reducing main thread load when syncing large state objects - Buffering and batching state updates for better performance This is particularly beneficial when: - Your application has many tabs open simultaneously - State updates are frequent or computationally intensive - You need to perform validation or transformation on synced data - The application handles large datasets that need to be synced You can find the complete code for this plugin in the [GitHub repository](https://github.com/alexanderop/pluginPiniaTabs). It also has examples of how to use it with Web Workers. --- --- title: The Browser That Speaks 200 Languages: Building an AI Translator Without APIs description: Learn how to build a browser-based translator that works offline and handles 200 languages using Vue and Transformers.js tags: ['vue', 'ai'] --- # The Browser That Speaks 200 Languages: Building an AI Translator Without APIs ## Introduction Most AI translation tools rely on external APIs. This means sending data to servers and paying for each request. But what if you could run translations directly in your browser? This guide shows you how to build a free, offline translator that handles 200 languages using Vue and Transformers.js. ## The Tools - Vue 3 for the interface - Transformers.js to run AI models locally - Web Workers to handle heavy processing - NLLB-200, Meta's translation model ```mermaid --- title: Architecture Overview --- graph LR Frontend[Vue Frontend] Worker[Web Worker] TJS[Transformers.js] Model[NLLB-200 Model] Frontend -->|"Text"| Worker Worker -->|"Initialize"| TJS TJS -->|"Load"| Model Model -->|"Results"| TJS TJS -->|"Stream"| Worker Worker -->|"Translation"| Frontend classDef default fill:#344060,stroke:#AB4B99,color:#EAEDF3 classDef accent fill:#8A337B,stroke:#AB4B99,color:#EAEDF3 class TJS,Model accent ``` ## Building the Translator ![AI Translator](../../assets/images/vue-ai-translate.png) ### 1. Set Up Your Project Create a new Vue project with TypeScript: ```bash npm create vite@latest vue-translator -- --template vue-ts cd vue-translator npm install npm install @huggingface/transformers ``` ### 2. Create the Translation Worker The translation happens in a background process. Create `src/worker/translation.worker.ts`: ```typescript // Singleton pattern for the translation pipeline class MyTranslationPipeline { static task: PipelineType = 'translation'; // We use the distilled model for faster loading and inference static model = 'Xenova/nllb-200-distilled-600M'; static instance: TranslationPipeline | null = null; static async getInstance(progress_callback?: ProgressCallback) { if (!this.instance) { this.instance = await pipeline(this.task, this.model, { progress_callback }) as TranslationPipeline; } return this.instance; } } // Type definitions for worker messages interface TranslationRequest { text: string; src_lang: string; tgt_lang: string; } // Worker message handler self.addEventListener('message', async (event: MessageEvent) => { try { // Initialize the translation pipeline with progress tracking const translator = await MyTranslationPipeline.getInstance(x => { self.postMessage(x); }); // Configure streaming for real-time translation updates const streamer = new TextStreamer(translator.tokenizer, { skip_prompt: true, skip_special_tokens: true, callback_function: (text: string) => { self.postMessage({ status: 'update', output: text }); } }); // Perform the translation const output = await translator(event.data.text, { tgt_lang: event.data.tgt_lang, src_lang: event.data.src_lang, streamer, }); // Send the final result self.postMessage({ status: 'complete', output, }); } catch (error) { self.postMessage({ status: 'error', error: error instanceof Error ? error.message : 'An unknown error occurred' }); } }); ``` ### 3. Build the Interface Create a clean interface with two main components: #### Language Selector (`src/components/LanguageSelector.vue`) ```vue ``` #### Progress Bar (`src/components/ProgressBar.vue`) ```vue ``` ### 4. Put It All Together In your main app file: ```vue