DEV Community

Alain Airom
Alain Airom

Posted on

Update of “Fun project of the week, Mermaid flowcharts generator!” — V2 and more…

Using LLMs to generate Mermaid flowcharts and don’t pay fees to sites!

Introduction

Following a previous post on how to generate Mermaid charts using Ollama and local LLMs (because I get really bored with sites which ask me to subscribe and pay a fee), I decided to enhance and update my application. There are two essential reasons for that;

  • My first app was containing hard-coded information regarding the LLM I want to use.
  • The application was not iterative.

🛠️ Essential Enhancements

The core of your enhancement should involve abstracting the LLM choice and creating a clean, repeatable workflow.

Dynamic LLM Selection (Addressing Hardcoded Info)

Instead of having a single hardcoded model, your application should dynamically discover and utilize any model available via your local Ollama instance.

  • Implement Model Discovery: The application must send a request to the Ollama API’s /api/tags endpoint (http://localhost:11434/api/tags). This endpoint returns a JSON list of all locally installed models.
  • Create a Selection Interface-CLI: Present the discovered list of models with numbered indices and prompt the user to choose one by number.
  • Create a Selection Interface-CLI with a GUI (Streamlit/other): Use a dropdown or radio button group populated by the retrieved model names.
  • Pass the Model Name: The chosen model name (e.g., llama3:8b-instruct-q4_0) must then be used as a variable in the payload for all subsequent API calls to the /api/chat endpoint.

Iterative Workflow and Error Handling (Addressing Iterativeness)

A non-iterative application forces a restart for every chart, which is frustrating. Iterativeness isn’t just about looping; it’s about handling success/failure gracefully within the same session.

  • Main Execution Loop: Wrap your primary logic (prompt user > call LLM > generate image) in a while True loop that only breaks when the user explicitly chooses to quit.
  • Session State (GUI): If you use a GUI framework like Streamlit (as we discussed previously), we must use Session State (st.session_state) to preserve the generated Mermaid code and the image path across button clicks and re-renders. This maintains the context of the user's current diagram.
  • Error Prevention/Debugging-Input Validation: Check if the user’s prompt is empty.
  • Error Prevention/Debugging-Connection Check: Check if the Ollama server is running before trying to fetch models or generate code.
  • File Handling Safety: Since it is creating temporary files for mmdc, ensure the cleanup logic is debuggable (e.g., only delete temp files if DEBUG_MODE is disabled).

And my ideas for V3… (to be continued)

| Enhancement                 | Description                                                  | Value Proposition                                            |
| --------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| **Code Review/Repair Mode** | If `mmdc` fails to render (due to a syntax error), automatically send the Mermaid code *and* the `mmdc` error log back to the LLM (with a specific system prompt) to ask it to fix the syntax. | Reduces user frustration and automatically fixes common LLM-induced syntax errors. |
| **Diagram History**         | Store the generated text prompt, the output code, and the corresponding image file path in a simple local database (like SQLite) or a structured file (like JSON/YAML). | Allows users to easily revisit and reuse past diagrams without regenerating them. |
| **Output Format Options**   | Add options to output the diagram in formats other than PNG, such as **SVG** (better for scaling) or **PDF**. | Increases versatility for users needing high-quality vector graphics. |
| **Persistent Settings**     | Save the last used LLM model to a configuration file (e.g., `config.json`). | Saves the user time by automatically selecting their preferred model upon startup. |
Enter fullscreen mode Exit fullscreen mode

Code(s) and Implementation(s)

1 — The console version

  • Let’s jump into the first console mode application. As usual build a virtual Python environment.
pip install --upgrade pip
pip install requests

npm install -g @mermaid-js/mermaid-cli
Enter fullscreen mode Exit fullscreen mode
  • And the code 🧑‍💻
# app_V3.py
import subprocess
import os
import requests
import json
import re
import glob
import sys
import time
from pathlib import Path

DEBUG_MODE = True 

OLLAMA_BASE_URL = "http://localhost:11434"
OLLAMA_CHAT_URL = f"{OLLAMA_BASE_URL}/api/chat"
OLLAMA_TAGS_URL = f"{OLLAMA_BASE_URL}/api/tags"

INPUT_DIR = Path("./input")
OUTPUT_DIR = Path("./output")

def check_mmdc_installed():
    """Checks if 'mmdc' is installed."""
    try:
        subprocess.run(['mmdc', '--version'], check=True, capture_output=True, timeout=5)
        return True
    except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.CalledProcessError):
        print("Error: Mermaid CLI (mmdc) not found or misconfigured.")
        print("Try: npm install -g @mermaid-js/mermaid-cli")
        return False

# MODEL SELECTION
def get_installed_models():
    """Fetches locally installed Ollama models."""
    try:
        response = requests.get(OLLAMA_TAGS_URL, timeout=5)
        response.raise_for_status()
        return sorted([m['name'] for m in response.json().get('models', [])])
    except:
        return []

def select_model_interactive():
    """Interactive menu to choose a model."""
    print("\n--- Ollama Model Selection ---")
    models = get_installed_models()

    if not models:
        return input("No models found. Enter model name manually (e.g., llama3): ").strip() or "llama3"

    for idx, model in enumerate(models, 1):
        print(f"{idx}. {model}")

    while True:
        choice = input(f"\nSelect a model (1-{len(models)}) or type custom name: ").strip()
        if choice.isdigit() and 1 <= int(choice) <= len(models):
            return models[int(choice) - 1]
        elif choice:
            return choice

def clean_mermaid_code(code_string):
    """Clean common LLM formatting errors from Mermaid code."""
    cleaned = code_string.replace(u'\xa0', ' ').replace(u'\u200b', '')

    cleaned = cleaned.replace("```

mermaid", "").replace("

```", "")

    cleaned = re.sub(r'[ \t\r\f\v]+', ' ', cleaned)

    lines = cleaned.splitlines()
    rebuilt = []
    for line in lines:
        s_line = line.strip()
        if s_line:
            rebuilt.append(s_line)

    final = '\n'.join(rebuilt)
    final = re.sub(r'(\])([A-Za-z0-9])', r'\1\n\2', final)
    return final.strip()

def generate_mermaid_code(user_prompt, model_name):
    """Calls Ollama to generate the code."""
    system_msg = (
        "You are a Mermaid Diagram Generator. Output ONLY valid Mermaid code. "
        "Do not include explanations. Start with 'graph TD' or 'flowchart LR'. "
        "Use simple ASCII characters for node IDs."
    )

    payload = {
        "model": model_name,
        "messages": [{"role": "system", "content": system_msg}, {"role": "user", "content": user_prompt}],
        "stream": False,
        "options": {"temperature": 0.1}
    }

    try:
        print(f"Thinking ({model_name})...")
        response = requests.post(OLLAMA_CHAT_URL, json=payload, timeout=60)
        response.raise_for_status()
        content = response.json().get("message", {}).get("content", "").strip()

        match = re.search(r"```

mermaid\n(.*?)\n

```", content, re.DOTALL)
        code = match.group(1) if match else content
        return clean_mermaid_code(code)

    except Exception as e:
        print(f"Error communicating with Ollama: {e}")
        return None

def translate_mermaid_to_image(mermaid_definition, output_path_base, output_format='png'):
    """Generates image from Mermaid code."""
    if not check_mmdc_installed(): return False

    output_path_base.parent.mkdir(parents=True, exist_ok=True)
    output_file = output_path_base.with_suffix(f'.{output_format}')

    temp_file = f"debug_{int(time.time())}.mmd"

    try:
        with open(temp_file, "w", encoding='utf-8') as f:
            f.write(mermaid_definition)

        command = ['mmdc', '-i', temp_file, '-o', str(output_file)]

        process = subprocess.run(command, capture_output=True, text=True)

        if process.returncode != 0:
            print(f"\n❌ ERROR: mmdc failed for {output_file.name}")
            print(f"--- STDERR ---\n{process.stderr}\n--------------")
            if DEBUG_MODE:
                print(f"⚠️  DEBUG MODE: Kept temporary file at: {os.path.abspath(temp_file)}")
                print("   Open this file in a text editor to check for syntax errors.")
            return False

        print(f"✅ Saved: {output_file}")
        return True

    except Exception as e:
        print(f"Unexpected error: {e}")
        return False
    finally:
        if not DEBUG_MODE and os.path.exists(temp_file):
            os.remove(temp_file)

def main():
    print("Welcome to the Ollama Mermaid Generator (Debug Mode)")
    if not check_mmdc_installed(): return

    selected_model = select_model_interactive()
    OUTPUT_DIR.mkdir(exist_ok=True)

    while True:
        choice = input("\n(1) Describe flowchart  (2) Process files  (q) Quit\n> ").strip().lower()

        mermaid_codes = []

        if choice == '1':
            desc = input("Describe flowchart: ")
            if not desc: continue
            code = generate_mermaid_code(desc, selected_model)
            if code:
                name = f"chart_{int(time.time())}"
                mermaid_codes.append((code, OUTPUT_DIR / name))

        elif choice == '2':
            if not INPUT_DIR.exists(): INPUT_DIR.mkdir(); print(f"Created {INPUT_DIR}"); continue
            files = list(INPUT_DIR.glob('**/*.mmd'))
            if not files: print("No files found."); continue
            for f in files:
                try:
                    code = clean_mermaid_code(f.read_text(encoding='utf-8'))
                    mermaid_codes.append((code, OUTPUT_DIR / f.relative_to(INPUT_DIR).with_suffix('')))
                except Exception as e: print(f"Error reading {f}: {e}")

        elif choice == 'q': break

        for code, path in mermaid_codes:
            print(f"\nProcessing {path.name}...")
            if DEBUG_MODE:
                print(f"--- Code Preview ---\n{code[:100]}...\n--------------------")

            success = translate_mermaid_to_image(code, path)

            if not success:
               input("\n⚠️ Generation failed. Read errors above and press Enter to continue...")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode
  • The output 📤
 python app_V3.py
Welcome to the Ollama Mermaid Generator (Debug Mode)

--- Ollama Model Selection ---
1. all-minilm:latest
2. deepseek-r1:latest
3. embeddinggemma:latest
4. granite-embedding:278m
5. granite-embedding:latest
6. granite3.2-vision:2b
7. granite3.3:latest
8. granite4:latest
9. granite4:micro-h
10. ibm/granite4:latest
11. ibm/granite4:micro
12. ibm/granite4:tiny-h
13. llama3.2-vision:latest
14. llama3:8b-instruct-q4_0
15. llama3:latest
16. ministral-3:latest
17. mistral:7b
18. mxbai-embed-large:latest
19. nomic-embed-text:latest
20. qwen3-vl:235b-cloud

Select a model (1-20) or type custom name: 8

(1) Describe flowchart  (2) Process files  (q) Quit
>
Enter fullscreen mode Exit fullscreen mode
  • The generated sample result if you want to generate a flowchart mermaid (.mmd) file ☕
flowchart TD
subgraph Client_Application [Your_main2]
A[Input_Document_e_g_PDF] --> B[Docling_DocumentConverter_convert]
B --> C{Docling_Internal_Pipelines_and_Parser_Backends}
C -- PDF_Image_Processing --> D[OCR_Engine_e_g_EasyOCR]
C -- Layout_Analysis --> E[Layout_Model]
C -- Table_Recognition --> F[TableFormer_Model]
D & E & F --> G[Docling_Document_Object_Structured_Data]
G --> H[Iterate_Document_Elements]
H --> I{Text_Table_Item}
I -- Yes --> J[Call_translate_function]
J --> K[Ollama_Client_Python_ollama_library]
K -- REST_API_Call --> L[Ollama_Server]
L -- Loads --> M[Granite3_Dense_LLM]
M -- Translated_Text --> K
K --> J
J --> N[Update_Docling_Document_Object]
N --> H
H -- No_More_Elements --> O[Docling_Document_save_as_markdown]
O --> P[Translated_Markdown_Output]
end
subgraph Ollama_Service
L --- M
end
style A fill:#e0f7fa,stroke:#333,stroke-width:2px
style P fill:#e0f7fa,stroke:#333,stroke-width:2px
style B fill:#b3e5fc,stroke:#333,stroke-width:2px
style C fill:#c7eafc,stroke:#333,stroke-width:2px
style D fill:#dbeefc,stroke:#333,stroke-width:2px
style E fill:#dbeefc,stroke:#333,stroke-width:2px
style F fill:#dbeefc,stroke:#333,stroke-width:2px
style G fill:#e0f7fa,stroke:#333,stroke-width:2px
style H fill:#a7d9f7,stroke:#333,stroke-width:2px
style I fill:#b7e2f7,stroke:#333,stroke-width:2px
style J fill:#c7eafc,stroke:#333,stroke-width:2px
style K fill:#d7f1fc,stroke:#333,stroke-width:2px
style L fill:#e7f7fd,stroke:#333,stroke-width:2px
style M fill:#f7fcfd,stroke:#333,stroke-width:2px
style N fill:#a7d9f7,stroke:#333,stroke-width:2px
style O fill:#b3e5fc,stroke:#333,stroke-width:2px
Enter fullscreen mode Exit fullscreen mode
  • Or if you want to give a definition and generate a visual flow;

2 — The GUI version using Streamlit

  • This version uses Streamlit to provide an enhanced interactive interface. Inside the virtual environment, we need to install the additional package.
# already done above
pip install --upgrade pip
pip install requests
npm install -g @mermaid-js/mermaid-cli
# new installation
pip install streamlit
Enter fullscreen mode Exit fullscreen mode
  • And the code 🆓
# appST.py
import streamlit as st
import subprocess
import requests
import json
import re
import os
import time
from pathlib import Path

# --- Configuration ---
OLLAMA_BASE_URL = "http://localhost:11434"
OLLAMA_CHAT_URL = f"{OLLAMA_BASE_URL}/api/chat"
OLLAMA_TAGS_URL = f"{OLLAMA_BASE_URL}/api/tags"
OUTPUT_DIR = Path("./output")
OUTPUT_DIR.mkdir(exist_ok=True)

st.set_page_config(
    page_title="Ollama Mermaid Architect",
    page_icon="🎨",
    layout="wide"
)

if 'mermaid_code' not in st.session_state:
    st.session_state['mermaid_code'] = ""
if 'generated_image_path' not in st.session_state:
    st.session_state['generated_image_path'] = None


def check_mmdc_installed():
    """Checks if mmdc is available."""
    try:
        subprocess.run(['mmdc', '--version'], check=True, capture_output=True, timeout=5)
        return True
    except:
        return False

def get_installed_models():
    """Fetches models from Ollama."""
    try:
        response = requests.get(OLLAMA_TAGS_URL, timeout=2)
        if response.status_code == 200:
            return [m['name'] for m in response.json().get('models', [])]
        return []
    except:
        return []

def clean_mermaid_code(code_string):
    """Cleans up LLM output."""
    clean = re.sub(r'```

mermaid', '', code_string, flags=re.IGNORECASE)
    clean = re.sub(r'

```', '', clean)

    clean = clean.replace(u'\xa0', ' ').replace(u'\u200b', '')

    lines = [line.strip() for line in clean.splitlines() if line.strip()]
    return '\n'.join(lines)

def generate_code(prompt, model):
    """Calls Ollama API."""
    system_msg = (
        "You are a Mermaid Diagram Generator. "
        "Output ONLY valid Mermaid code. Start with 'graph TD' or 'flowchart LR'. "
        "Do NOT include explanations or markdown ticks."
    )

    payload = {
        "model": model,
        "messages": [
            {"role": "system", "content": system_msg},
            {"role": "user", "content": prompt}
        ],
        "stream": False,
        "options": {"temperature": 0.2}
    }

    try:
        response = requests.post(OLLAMA_CHAT_URL, json=payload, timeout=60)
        if response.status_code == 200:
            content = response.json()['message']['content']
            return clean_mermaid_code(content)
        else:
            st.error(f"Ollama Error: {response.text}")
            return None
    except Exception as e:
        st.error(f"Connection Error: {e}")
        return None

def render_diagram(mermaid_code):
    """Runs mmdc to generate an SVG for display."""
    timestamp = int(time.time())
    temp_mmd = OUTPUT_DIR / f"temp_{timestamp}.mmd"
    output_svg = OUTPUT_DIR / f"diagram_{timestamp}.svg"

    try:
        with open(temp_mmd, "w", encoding='utf-8') as f:
            f.write(mermaid_code)

        cmd = ['mmdc', '-i', str(temp_mmd), '-o', str(output_svg), '-b', 'transparent']
        result = subprocess.run(cmd, capture_output=True, text=True)

        if result.returncode == 0:
            return str(output_svg)
        else:
            st.error("Mermaid Compilation Failed:")
            st.code(result.stderr)
            return None

    except Exception as e:
        st.error(f"System Error: {e}")
        return None
    finally:
        if temp_mmd.exists():
            temp_mmd.unlink()

# --- GUI Layout ---

# Sidebar
with st.sidebar:
    st.header("⚙️ Configuration")

    if check_mmdc_installed():
        st.success("Mermaid CLI: Detected ✅")
    else:
        st.error("Mermaid CLI: Not Found ❌")
        st.info("Run: `npm install -g @mermaid-js/mermaid-cli`")
        st.stop()

    models = get_installed_models()
    if models:
        selected_model = st.selectbox("Select Ollama Model", models, index=0)
    else:
        st.warning("Ollama not detected or no models found.")
        selected_model = st.text_input("Manually enter model name", "llama3")

    st.markdown("---")
    st.markdown("### Tips")
    st.markdown("- Be specific about direction (Top-Down vs Left-Right).")
    st.markdown("- Mention specific node shapes if needed.")

st.title("🎨 Ollama Mermaid Architect")
st.markdown("Generate, Visualize, and Edit flowcharts using local LLMs.")

user_prompt = st.text_area("Describe your flowchart:", height=100, placeholder="e.g. Create a flowchart for a login process including password reset...")

col1, col2 = st.columns([1, 5])
with col1:
    generate_btn = st.button("🚀 Generate", type="primary")

if generate_btn and user_prompt:
    with st.spinner(f"Asking {selected_model} to design your chart..."):
        generated_code = generate_code(user_prompt, selected_model)

        if generated_code:
            st.session_state['mermaid_code'] = generated_code
            svg_path = render_diagram(generated_code)
            st.session_state['generated_image_path'] = svg_path

if st.session_state['mermaid_code']:
    st.markdown("---")

    tab_visual, tab_code = st.tabs(["🖼️ Diagram Visualization", "📝 Edit Code"])

    with tab_visual:
        if st.session_state['generated_image_path'] and os.path.exists(st.session_state['generated_image_path']):
            st.image(st.session_state['generated_image_path'], use_container_width=True)

            with open(st.session_state['generated_image_path'], "rb") as file:
                btn = st.download_button(
                    label="📥 Download SVG",
                    data=file,
                    file_name="flowchart.svg",
                    mime="image/svg+xml"
                )
        else:
            st.warning("No image generated yet or generation failed.")

    with tab_code:
        st.info("You can edit the code below and press 'Update' to re-render.")
        edited_code = st.text_area("Mermaid Code", st.session_state['mermaid_code'], height=300)

        if st.button("🔄 Update Diagram"):
            st.session_state['mermaid_code'] = edited_code
            new_svg_path = render_diagram(edited_code)
            if new_svg_path:
                st.session_state['generated_image_path'] = new_svg_path
                st.rerun() # Refresh to show new image
Enter fullscreen mode Exit fullscreen mode
  • Run the code 🏃‍➡️
streamlit run appST.py

  You can now view your Streamlit app in your browser.

  Local URL: http://localhost:8501
  Network URL: http://192.168.1.148:8501
Enter fullscreen mode Exit fullscreen mode

That’s a wrap, thanks for reading 😅

🎯 Conclusion: Empowering Local Diagram Generation

In summary, we’ve successfully developed a powerful, fully local and iterative application for generating Mermaid flowcharts using Ollama and local Large Language Models (LLMs). This process demonstrates that tasks such as diagram generation — can be achieved effectively and flexibly using open-source tools and local computation, entirely eliminating the need for subscription-based services.

Links

Top comments (0)