janwillemaltink
  • home
  • blog
  • Finetuning gids
janwillemaltink.
InstagramGitHubX
Development met Claude en Artifacts
👨🏼‍💻

Development met Claude en Artifacts

Tags
AILLMsAnthropic

✒️Door: @Jan Willem Altink

📅Datum: 28 augustus 2024

🕜Leestijd: 11 minuten

Inleiding

Met de opkomst van Large Language Models zoals ChatGPT en Claude, is programmeren toegankelijker dan ooit. Dat LLMs en code een goede combinatie zijn, hadden we al snel in de gaten. GitHub Copilot, bijvoorbeeld, is alweer sinds medio 2021 beschikbaar en heeft sindsdien talloze ontwikkelaars geholpen bij het schrijven van efficiëntere en nauwkeurigere code. Meer recente voorbeelden zijn Aider, een tool die interactief samenwerken aan code mogelijk maakt, en CursorAI, een AI gedreven IDE.

Over CursorAI gesproken, check dit fantastische filmpje van een 8 jarige gebruiker:

Het is logisch dat LLM’s goed toepasbaar zijn bij coderen, ze zijn natuurlijk grotendeels getraind op data van het internet, en het internet staat vol met code. Veel modellen zijn zelfs specifiek getraind op programmeren in algemene zin of specifieke programmeertalen.

Wat mij betreft een leuke ontwikkeling: het leren van een bepaalde taal is door LLM’s geen barriere meer tussen het hebben van een idee en de uitvoer ervan. Voor mij persoonlijk geldt dat in ieder geval absoluut. Ik heb het uitdenken van algoritmes of logica altijd leuk gevonden, maar al die ingewikkelde syntax schrok me toch vaak een beetje af. Dat is inmiddels veel minder een probleem. Voor ervaren developers voor wie dat allemaal geen probleem is, bieden LLM’s een ander groot voordeel: simpele en repeterende taken kunnen veelal veel sneller gedaan worden met behulp van AI.

Claude 3.5 Sonnet

Wat mij betreft is, zelfs na de surprise update van afgelopen week van OpenAI aan GPT4-o, Claude Sonnet het fijnste model om mee te werken als het aankomt op zowel schrijfstijl als coderen. Claude Sonnet heeft een aantal voordelen:

  • Een gigantische context window (200.000 tokens; dat is ongeveer 150.000 woorden, of 300 pagina’s tekst)
  • Vision
  • Omdat het niet het model is met het grootst aantal parameters (dat is Opus) heel betaalbaar
  • ..en uiteraard alle andere capaciteiten die je zou verwachten, zoals toolcalling, een begrijpelijke API etc.

Vandaag wil ik er een ander voordeel uitlichten, de user interface. Het heeft lange tijd geduurd voor deze beschikbaar was in Nederland. De API deed het al wel, maar dat werkte toch net wat omslachtiger. Echter: het was het wachten waard want in de user interface zitten een aantal super handige concepten: projects, artifacts en de console.

Anthropic Console

De Anthropic Console is een krachtige tool die het werken met Claude aanzienlijk vergemakkelijkt. Een van de belangrijkste voordelen is de ingebouwde prompt generator, die gebruikers helpt bij het formuleren van effectieve instructies voor Claude. Dit zorgt voor consistentere en meer gerichte resultaten.

image

Een ander handig kenmerk is het gebruik van inline variabelen in prompts. Dit maakt het mogelijk om delen van je prompts te hergebruiken en aan te passen, wat de efficiëntie en flexibiliteit van je interacties met Claude verhoogt. Je kunt bijvoorbeeld een basisstructuur voor een prompt maken en specifieke delen gemakkelijk aanpassen voor verschillende use-cases.

Tot slot biedt de console een evaluatiefunctie. Hiermee kun je de kwaliteit en nauwkeurigheid van Claude's antwoorden beoordelen, wat waardevol is voor het verfijnen van je prompts en het verbeteren van je resultaten over tijd.

Claude Projects

image

Bron: Anthropic

Claude Projects is een functie die vooral nuttig is voor grotere, complexere taken die meerdere interacties vereisen. Het stelt gebruikers in staat om gerelateerde gesprekken en outputs te groeperen binnen één project, wat de organisatie en het overzicht verbetert.

Voor LLM-begrippen zijn 'complexere' projecten taken die mogelijk meerdere stappen, iteraties of verschillende typen output vereisen. Denk bijvoorbeeld aan het ontwikkelen van een volledige webapplicatie, het schrijven van een uitgebreid onderzoeksrapport, of het ontwerpen van een marketingstrategie. Het gebruik van Projects maakt het gemakkelijker om de voortgang bij te houden, eerdere resultaten te raadplegen en de context van het gehele project te behouden.

Voor mij zit er echter een groot nadeel aan het gebruik van projects; de token limit voor gebruikers binnen Claude. Claude hanteert op dit moment een behoorlijk strikte token limit, waardoor je nogal eens tegen de gebruikerslimiet aanloopt. Super frustrerend als je net lekker bezig bent. Ik heb de indruk dat documentatie die je in een claude project verwerkt niet via toolcalling of RAG wordt opgehaald door de LLM, maar gewoon onderdeel is vand e contextwindow. Dit zorgt ervoor dat je deze tokenlimit nog veel sneller bereikt.

Bovendien heb ik gemerkt dat claude een grote context niet altijd goed verwerkt. ALs je bijvoorbeeld met een code vraagstuk aanklopt is het niet ongebruikelijk dat claude in de oplossing bepaalde class definities of utilty functies herschrijft/dupliceert in plaats van een import. Ik geef er de voorkeur aan om enigszins te cherrypicken in de context die ik Claude meegeef omdat dat a: de token usage drastisch verlaagt en b: de kwaliteit van de output van Claude verbetert. Het vergt iets meer moeite, maar ik heb een werkwijze gevonden die redelijk soepel werkt (waarover straks meer).

Claude Artifacts

Claude Artifacts zijn een bijzonder handige functie die de interactie met het model aanzienlijk verbetert. Artifacts stellen Claude in staat om substantiële, zelfstandige inhoud te creëren en te beheren binnen een gesprek. Dit is vooral nuttig voor code, diagrammen, of tekstdocumenten die apart van de hoofdconversatie worden weergegeven.

Het grote voordeel hiervan is dat gebruikers deze inhoud gemakkelijk kunnen bekijken, bewerken en hergebruiken zonder door een lange chat te hoeven scrollen. Artifacts zijn ideaal voor het iteratief ontwikkelen van code, het verfijnen van technische specificaties, of het samenstellen van gestructureerde documenten. Ze bieden een duidelijke scheiding tussen de discussie over een taak en het daadwerkelijke product, wat de workflow overzichtelijker en efficiënter maakt. Bovendien kunnen Artifacts worden geëxporteerd of gedeeld, wat samenwerking en het hergebruik van waardevolle output vergemakkelijkt. Hier wat gave voorbeelden.

Tips voor co-development met Claude

1. Maak een guidelines.md

ik vind het fijn om altijd een kleine markdown te maken met wat guidelines over de structuur van mijn project, naming conventions, gebruik van code comments, docstrings, types, classes, geprefereerde pakketten etc. De reden hiervoor is dat een LLM best wel vaak geneigd is om daar in steeds net wat te variereren, wat na verloop van tijd als je de code blind zou copy pasten zorgt voor enorme spaghetti. Los van het feit dat je deze markdown aan de LLM kunt meegeven bij het genereren van output, helpt het jezelf ook om dit vast te leggen, zodat je de door de LLM gegenereerde output op deze punten kunt valideren.

‣
Hier 2 voorbeelden (Svelte en Python voorbeeld)

2. Cherrypick de context

Zoals al eerder beschreven ben ik er fan van om de context die we meegeven aan de LLM te cherry picken. Dit zorgt dat de focus op de juiste onderdelen van de code ligt en houdt het aantal gebruikte tokens behapbaar. Omdat het lastig is om steeds opnieuw 20 bestandjes te copy pasten, hieronder een korte uitleg over mijn werkwijze. Binnen elk project heb ik in de root altijd een werkmapje “dev” of iets dergelijks. ik zet daarin altij een tekstfile dev/generate_markdown.txt . in deze tekstfile zet ik, als ik bestandjes wil delen voor een request/vraag aan claude alle paden van die bestandjes, bijvooorbeeld (altijd de guidelines uit stap 1 plus een lijst bestandjes):

Ik heb ook een klein shell script geschreven waarmee ik de locatie plus de inhoud van de bestanden die ik wil delen in een markdown zet. Het scriptje rekent ook al vast even het aantal tokens en karakters uit en print deze naar de terminal, ook copy paste het de inhoud van de markdown. zodat ik in Claude alleen nog maar even een nieuwe chat hoef te starten en te pasten om mijn vraag te stellen en precies de juiste context mee te geven. Het voordeel van handmatig knippen en plakken zit hem er vooral in als je wat meer dan 1 vragen hebt, het is veel sneller om even een nieuwe markdown te genereren door het script te rerunnen dan steeds alles opnieuw te copy pasten. Vermoed dat het alleen op Mac werkt, maar dit is het script dat ik op dit moment gebruik:

‣
Klap open

3. Wees extra kritisch op de volgende aspecten van de door Claude gegenereerde code

  • Is er geen functionaliteit verdwenen? Ik zet het ook altijd in het prompt, want Claude heeft de neiging soms code ongevraagd weg te laten. kijk secuur of er onverwacht geen code is verdwenen
  • Is er duplicate code bijgekomen? Met het cherrypicken van de context wordt het vaak al een stuk beter, maar het kan voorkomen dat bepaalde uitilty functies bijvoorbeeld toch dubbel worden gedefinieerd, ik vind vaak dat Claude niet alle overkoepelende utils en classes etc. correct toepast.

4. ga niet te lang door op een fout spoor

Als gegenereerde code van Claude niet direct werkt en je ziet de fout niet direct, ga niet telang door met het fixen van de fout, het steeds toevoegen van foutlogs aan de chat in de hoop dat een volgende iteratie wel werkt is zelden succesvol. Claude fixeert zich naarmate zo’n gesprek waarbij je steeds de fout logs deelt steeds meer op specifieke errors en vergeet de context, ook zie je claude vaak heen en weer pingpongen tussen 2 of 3 dezelfde fouten. Ikzelf doe nooit meer dan 2 pogingen binnen een chat. Het is handiger om bij foutief gegenereerde code even een nieuwe chat te starten en je prompt iets te optimaliseren.

5. wees alert op het gebruik van gedateerde packages of concepten

ik heb de indruk dat de cut off date van Claude wat minder recent is dan nieuwere versies van ChatGPT. Check bij het gebruik van bepaalde packages of toepassingen door de LLM goed of de gebruikte code up to date is, metname als je werkt met packages die veel veranderen, zoals LangChain. Ook de Docker Compose files en instructies die Claude geeft zijn meestal gedateerd heb ik gemerkt.

Tot slot

Dat waren mijn ervaringen met het gebruik van Large Language Models, met name Claude 3.5 Sonnet, voor programmeren. Maar wat vandaag werkt, kan morgen anders zijn. Nieuwe modellen en functionaliteiten verschijnen regelmatig, en best practices evolueren mee. Bovendien leer ik ook nog bijna dagelijks bij. Daarom is het lastig om definitieve conclusies te trekken.

Ik ben benieuwd naar jullie ervaringen. Heb je andere manieren gevonden om LLMs in je ontwikkelproces te integreren? Tegen welke uitdagingen ben je aangelopen?

-JW

# Svelte Project Guidelines (TypeScript)

## Project Structure
```
project_root/
│
├── src/
│   ├── lib/
│   │   ├── components/
│   │   │   └── MyComponent.svelte
│   │   └── utils/
│   │       └── helpers.ts
│   ├── routes/
│   │   ├── +layout.svelte
│   │   ├── +layout.ts
│   │   ├── +page.svelte
│   │   └── +page.server.ts
│   ├── app.html
│   └── app.css
│
├── static/
│   └── favicon.png
│
├── tests/
│   └── test.ts
│
├── .env
├── .gitignore
├── README.md
├── svelte.config.js
├── tailwind.config.js
├── tsconfig.json
└── package.json
```

## Algemene Richtlijnen
- Geef altijd volledige componenten, functies en klassen terug, gebruik nooit placeholders.
- Splits logica op in kleinere, herbruikbare componenten en functies waar mogelijk.
- Gebruik betekenisvolle namen voor variabelen, functies, componenten en bestanden.
- Schrijf self-documenting code waar mogelijk.

## Naming Conventions
- Gebruik PascalCase voor component bestandsnamen en export names
- Gebruik camelCase voor variabelen en functies
- Gebruik kebab-case voor CSS klassen en bestandsnamen (behalve componenten)

## Component Structure
- Gebruik de volgende volgorde in componenten:
  1. <script lang="ts">
  2. <style>
  3. HTML markup

Voorbeeld van een component:

```svelte
<script lang="ts">
  import type { User } from '$lib/types';
  import { onMount } from 'svelte';

  export let user: User;

  let isLoading = true;

  onMount(() => {
    // Component logic here
    isLoading = false;
  });
</script>

<div class="user-card">
  {#if isLoading}
    <p>Loading...</p>
  {:else}
    <h2>{user.name}</h2>
    <p>{user.email}</p>
  {/if}
</div>

<style lang="postcss">
  .user-card {
    @apply p-4 bg-white shadow rounded;
  }
</style>
```

## TypeScript
- Gebruik strict TypeScript configuratie
- Definieer interfaces en types in aparte `.ts` bestanden of bovenaan het component
- Gebruik type annotaties voor alle props, events, en lokale variabelen
- Vermijd het gebruik van `any` type; gebruik `unknown` indien nodig
- Maak gebruik van TypeScript's discriminated unions voor complexe state management

## Styling
- Gebruik Tailwind CSS voor styling
- Plaats globale styles in `app.css`
- Gebruik scoped styling binnen componenten waar nodig

## State Management
- Gebruik Svelte stores voor globale state
- Prefereer lokale state binnen componenten waar mogelijk
- Gebruik TypeScript voor type-veilige stores

## Routing
- Gebruik SvelteKit file-based routing
- Plaats route componenten in de `routes` map
- Gebruik TypeScript voor route parameters en data loading

### Layout en Page Structure
- Gebruik `+layout.svelte` voor gedeelde layout componenten
- Gebruik `+layout.ts` voor layout-specifieke logica en data loading
- Gebruik `+page.svelte` voor pagina-specifieke componenten
- Gebruik `+page.server.ts` voor server-side logica en data fetching

Voorbeeld van `+layout.ts`:

```typescript
import type { LayoutLoad } from './$types';

export const load: LayoutLoad = async ({ fetch }) => {
  const res = await fetch('/api/navigation');
  const navigation = await res.json();

  return {
    navigation
  };
};
```

Voorbeeld van `+page.server.ts`:

```typescript
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params }) => {
  // Fetch data or perform server-side operations
  const data = await fetchSomeData(params.id);

  return {
    props: { data }
  };
};
```

## UI Componenten
- Gebruik shadcn-svelte componenten waar mogelijk
- Maak custom componenten wanneer shadcn-svelte niet voorziet in de behoefte
- Zorg voor sterke typering van component props

## Code Comments
- Gebruik JSDoc stijl comments voor functies en componenten
- Houd inline comments beknopt en relevant
- Documenteer complexe types en interfaces

## Testing
- Schrijf unit tests voor alle componenten en utilities
- Gebruik Svelte Testing Library voor component tests
- Gebruik Jest voor unit tests van utility functies

Voorbeeld van een component test:

```typescript
import { render, fireEvent } from '@testing-library/svelte';
import MyComponent from './MyComponent.svelte';

test('it should update the count when the button is clicked', async () => {
  const { getByText } = render(MyComponent);
  
  const button = getByText('Increment');
  const countDisplay = getByText('Count: 0');
  
  await fireEvent.click(button);
  
  expect(countDisplay.textContent).toBe('Count: 1');
});
```

## Geprefereerde Pakketten
- Gebruik Axios voor HTTP requests
- Gebruik date-fns voor datum manipulatie
- Gebruik Zod voor runtime type checking en validatie

## Configuratie
- Gebruik `.env` voor omgevingsvariabelen
- Gebruik `svelte.config.js` voor Svelte configuratie
- Gebruik `tailwind.config.js` voor Tailwind configuratie
- Gebruik `tsconfig.json` voor TypeScript configuratie

## Modulariteit en Herbruikbaarheid
- Houd componenten klein en gericht op één functionaliteit
- Splits grote componenten op in kleinere, herbruikbare onderdelen
- Gebruik TypeScript generics voor herbruikbare componenten en functies

## Overig
- Volg de officiële Svelte style guide
- Gebruik ESLint en Prettier voor code formatting en linting
# Python Project Guidelines

## Project Structure
```
project_root/
│
├── src/
│   ├── my_package/
│   │   ├── __init__.py
│   │   ├── module1.py
│   │   └── module2.py
│   └── main.py
│
├── tests/
│   ├── test_module1.py
│   └── test_module2.py
│
├── docs/
│   └── api.md
│
├── .env
├── .gitignore
├── README.md
└── pyproject.toml
```

## Algemene Richtlijnen
- Geef altijd volledige functies/classes terug, gebruik nooit placeholders binnen functies.
- Splits logica op in kleinere, herbruikbare functies waar mogelijk.
- Gebruik betekenisvolle namen voor variabelen, functies en classes.
- Schrijf self-documenting code waar mogelijk.

## Projectconfiguratie en Dependency Management
- Gebruik `pyproject.toml` voor projectconfiguratie en dependency management.

- Mijn `pyproject.toml`:

```toml
[project]
name = "my_package"
version = "0.1.0"
description = "A short description of my package"
authors = [
    {name = "Your Name", email = "you@example.com"},
]
dependencies = [
    "requests>=2.25.1",
    "pydantic>=1.8.2",
]
requires-python = ">=3.9"

[project.optional-dependencies]
dev = [
    "pytest>=6.2.5",
    "black>=21.9b0",
    "flake8>=3.9.2",
]

[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
```

## Import Strategie
- Structureer je project als een Python package (met `__init__.py` bestanden).
- Gebruik absolute imports binnen je package.
- Vermijd het gebruik van relatieve imports.

Voorbeeld:

```python
# Goed
from my_package.module1 import function1
from my_package.module2 import Class1

# Vermijd
from .module1 import function1
from ..module2 import Class1
```

## Naming Conventions
- Gebruik `snake_case` voor variabelen en functies
- Gebruik `PascalCase` voor klassen
- Gebruik `UPPER_CASE` voor constanten
- Prefix private variabelen en methoden met een enkele underscore (_)
- Gebruik duidelijke, beschrijvende namen
- Vermijd afkortingen tenzij ze algemeen bekend zijn

Voorbeeld:
```python
MAX_ITERATIONS = 100

class UserAccount:
    def __init__(self, username):
        self.username = username
        self._password = None

    def set_password(self, new_password):
        self._password = new_password

def calculate_average(numbers):
    total = sum(numbers)
    return total / len(numbers)
```

## Code Comments en Docstrings
- Gebruik docstrings voor alle functies, methoden, klassen en modules
- Volg de Google style guide voor docstrings
- Gebruik inline comments spaarzaam, alleen voor complexe logica

Voorbeeld:
```python
def complex_function(param1: int, param2: str) -> Dict[str, Any]:
    """
    Perform a complex operation and return the result.

    Args:
        param1 (int): Description of param1
        param2 (str): Description of param2

    Returns:
        Dict[str, Any]: Description of the return value

    Raises:
        ValueError: If param1 is negative
    """
    # Complex logic here
    pass
```

## Types en Data Validatie
- Gebruik type hints voor alle functie parameters en return types
- Gebruik `typing` module voor complexe types
- Gebruik Pydantic voor data validatie en serialisatie

Voorbeeld:
```python
from typing import List, Dict
from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    email: str

def process_users(users: List[User]) -> Dict[int, str]:
    return {user.id: user.name for user in users}
```

## Functies en Modulariteit
- Houd functies klein en modulair (niet meer dan 20-30 regels waar mogelijk)
- Splits functies op in kleinere, herbruikbare eenheden waar mogelijk
- Volg het Single Responsibility Principle
- Geef altijd volledige functies terug, gebruik nooit placeholders

## Classes
- Volg het principe van encapsulatie
- Gebruik properties in plaats van getter/setter methoden waar mogelijk

Voorbeeld:
```python
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @property
    def area(self):
        return 3.14159 * self._radius ** 2
```

## Geprefereerde Pakketten
- Gebruik `requests` voor HTTP requests
- Gebruik `pytest` voor testing
- Gebruik `flake8` voor linting
- Gebruik `pydantic` voor data validatie en serialisatie

## Configuratie
- Gebruik `.env` voor omgevingsvariabelen en API keys
- Gebruik `pyproject.toml` voor project configuratie en dependency management

## Overig
- Volg PEP 8 richtlijnen voor code style
- Gebruik f-strings voor string formatting
- Schrijf unit tests voor alle functies en methoden
- Gebruik type annotaties consequent

Voorbeeld van een test:
```python
import pytest
from my_package.module1 import add_numbers

def test_add_numbers():
    assert add_numbers(2, 3) == 5
    assert add_numbers(-1, 1) == 0
    with pytest.raises(TypeError):
        add_numbers("2", 3)
```

/Users/jw/developer/ZenFiles/src/zenfiles/guidelines.md
/Users/jw/developer/ZenFiles/src/zenfiles/main.py
/Users/jw/developer/ZenFiles/src/zenfiles/explore/analyzer.py
/Users/jw/developer/ZenFiles/src/zenfiles/explore/config.py
/Users/jw/developer/ZenFiles/src/zenfiles/explore/file_utils.py
/Users/jw/developer/ZenFiles/src/zenfiles/explore/markdown_converter.py
/Users/jw/developer/ZenFiles/src/zenfiles/explore/overview_generator.py
/Users/jw/developer/ZenFiles/src/zenfiles/explore/tree_generator.py
#!/bin/zsh

# Ensure we have the full PATH
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"

# Location of the text file with paths to the files
input_file="generate_markdown.txt"

# Location of the output markdown file
output_file="output.md"

# Create (or overwrite) the output file
echo "# File Contents\n" > "$output_file"

# Initialize variables for summary
typeset -A file_types_count
failed_files=0
total_files=0
total_char_count=0
total_token_estimate=0

# Function to process a file
process_file() {
  local file=$1
  if [[ -f "$file" ]]; then
    file_extension="${file:e}"
    ((file_types_count[$file_extension]++))
    
    echo "## File: $file" >> "$output_file"
    echo '```' >> "$output_file"
    if command -v cat > /dev/null 2>&1; then
      cat "$file" >> "$output_file"
    else
      while IFS= read -r line; do
        echo "$line" >> "$output_file"
      done < "$file"
    fi
    echo '```' >> "$output_file"
    echo -e "\n" >> "$output_file"
    
    # Count the number of characters and estimate tokens
    if command -v wc > /dev/null 2>&1; then
      local char_count=$(wc -m < "$file" | tr -d ' ')
    else
      local char_count=${#$(<"$file")}
    fi
    local formatting_chars=$((${#file} + 11 + 4 + 4)) # For "## File: ", '```', and padding
    local total_chars=$((char_count + formatting_chars))
    total_char_count=$((total_char_count + total_chars))
    local token_estimate=$((total_chars / 4))
    total_token_estimate=$((total_token_estimate + token_estimate))
  else
    echo "File not found: $file" >> "$output_file"
    ((failed_files++))
  fi
}

# Loop through the list of files/folders in the input file
while IFS= read -r path || [[ -n "$path" ]]; do
  if [[ -d "$path" ]]; then
    # If it's a folder, process all files in the folder
    for file in "$path"/**/*(.); do
      ((total_files++))
      process_file "$file"
    done
  elif [[ -f "$path" ]]; then
    # If it's a file, process it directly
    ((total_files++))
    process_file "$path"
  else
    echo "Invalid path: $path" >> "$output_file"
    ((failed_files++))
  fi
done < "$input_file"

# Function for coutput
print_color() {
  local color=$1
  local text=$2
  echo -e "\033[${color}m${text}\033[0m"
}

# Generate summary
summary="Markdown file has been generated: $output_file

Summary:
Total files processed: $total_files
"

for ext in "${(k)file_types_count[@]}"; do
  summary+="Number of $ext files: ${file_types_count[$ext]}\n"
done

if (( failed_files > 0 )); then
  summary+="Number of failures: $failed_files\n"
fi

summary+="Total character count: $total_char_count
Total estimated tokens: $total_token_estimate"

# Print summary
print_color "1;34" "\n$summary"

# Copy markdown content to clipboard
if command -v /usr/bin/pbcopy > /dev/null 2>&1; then
  cat "$output_file" | /usr/bin/pbcopy
  print_color "1;32" "\nMarkdown content has been copied to the clipboard."
else
  print_color "1;31" "\nCould not copy the markdown content to the clipboard (pbcopy not found)."
fi