xiaohongshu
xiaohongshu
convert-elm-roc
Convert Elm code to idiomatic Roc. Use when migrating Elm frontend code to Roc applications, translating browser-based Elm to platform-agnostic Roc, or refactoring Elm web applications to Roc CLI/native tools. Extends meta-convert-dev with Elm-to-Roc specific patterns.
Full skill instructions
Convert Elm code to idiomatic Roc. This skill extends meta-convert-dev with Elm-to-Roc specific type mappings, idiom translations, and architectural patterns for moving from browser-based Elm applications to platform-agnostic Roc code.
meta-convert-dev - Foundational conversion patterns (APTV workflow, testing strategies)For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
meta-convert-devlang-elm-devlang-roc-devconvert-roc-elm| Elm | Roc | Notes |
|---|---|---|
String | Str | Direct mapping |
Int | I64 or U64 | Choose signed/unsigned based on domain |
Float | F64 | Direct mapping |
Bool | Bool | Same with capitalization |
List a | List a | Same syntax and operations |
{ field : Type } | { field : Type } | Records are nearly identical |
type Custom = Tag1 | Tag2 | [Tag1, Tag2] | Custom types → Tag unions |
Result err ok | Result ok err | REVERSED parameter order! |
Maybe a | [Some a, None] | Optional values |
Cmd Msg or Task err a | Task ok err | Effect systems differ |
case x of | when x is | Pattern matching syntax |
Task.perform | ! suffix operator | Explicit handling → Bang operator |
This is the most important thing to remember when converting Elm to Roc:
-- Elm: Result error ok
divide : Int -> Int -> Result String Int
# Roc: Result ok err (REVERSED!)
divide : I64, I64 -> Result I64 Str
The parameter order is completely reversed between Elm and Roc:
Result error ok - Error type first, success type secondResult ok err - Success type first, error type secondThis affects:
When you see Elm's Result String User, it becomes Roc's Result User Str.
❌ Wrong: Result Str User (copying Elm order)
✓ Correct: Result User Str (reversed order)
| Aspect | Elm TEA | Roc Platform Model |
|---|---|---|
| Target | Browser frontend only | Any platform (CLI, web, native) |
| Effects | Runtime-managed Cmd/Sub | Platform-provided Task |
| Entry point | main : Program () Model Msg | main : Task {} [] |
| State | Explicit Model | Implicit in Task chain |
| Updates | update : Msg → Model → (Model, Cmd Msg) | Task composition |
| I/O | Browser.* modules only | Platform exposes (File, Http, etc.) |
module Main exposing (main)
import Browser
import Html exposing (Html, div, input, text)
import Html.Events exposing (onInput)
import Html.Attributes exposing (placeholder, value)
-- MODEL
type alias Model =
{ name : String
, greeting : String
}
init : () -> ( Model, Cmd Msg )
init _ =
( { name = "", greeting = "Hello, World!" }, Cmd.none )
-- UPDATE
type Msg
= NameChanged String
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NameChanged newName ->
( { model
| name = newName
, greeting = "Hello, " ++ newName ++ "!"
}
, Cmd.none
)
-- VIEW
view : Model -> Html Msg
view model =
div []
[ div [] [ text model.greeting ]
, input
[ placeholder "Enter your name"
, value model.name
, onInput NameChanged
]
[]
]
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br"
}
import pf.Stdout
import pf.Stdin
import pf.Task exposing [Task]
main : Task {} []
main =
Stdout.line! "Hello, World!"
Stdout.line! "Enter your name:"
name = Stdin.line!
Stdout.line! "Hello, \(name)!"
Key shift: Elm's declarative Model-Update-View loop becomes Roc's imperative Task chain.
Note: The Roc version is CLI-based because Roc doesn't target the browser. For equivalent browser functionality, you'd need a Roc web platform (still in development).
| Elm | Roc | Notes |
|---|---|---|
True / False | Bool.true / Bool.false | Capitalization differs |
42 | 42 | Integer literals |
3.14 | 3.14 | Float literals |
"text" | "text" | String literals |
Int | I64 or U64 | Elm has arbitrary precision, Roc has sized types |
Float | F64 or F32 | Elm has single Float, Roc has sized types |
String | Str | Direct mapping |
Char | U32 | Roc treats chars as Unicode scalar values |
| Elm | Roc | Notes |
|---|---|---|
List a | List a | Identical syntax and semantics |
Dict comparable v | Dict k v | Roc requires k to implement Hash & Eq |
Set comparable | Set a | Roc requires a to implement Hash & Eq |
( a, b ) | (a, b) | Tuples (Roc supports arbitrary tuple sizes) |
Array a | List a | Elm's Array → Roc's List (Roc optimizes internally) |
| Elm | Roc | Notes |
|---|---|---|
{ name : String, age : Int } | { name : Str, age : U32 } | Nearly identical, just type name differences |
{ user | age = 31 } | { user & age: 31 } | Record update syntax differs (| vs &, = vs :) |
{ name, age } = user | { name, age } = user | Destructuring identical |
user.name | user.name | Field access identical |
Elm:
-- Named custom type (nominal)
type Color
= Red
| Green
| Blue
| Custom Int Int Int
type alias RGB =
{ r : Int, g : Int, b : Int }
Roc:
# Structural tag union
Color : [Red, Green, Blue, Custom(U8, U8, U8)]
# Record type alias
RGB : { r : U8, g : U8, b : U8 }
Key differences:
type declaration|[]Elm:
-- Built-in Maybe type
email : Maybe String
email = Just "[email protected]"
-- Pattern match
emailText : String
emailText =
case email of
Just addr ->
addr
Nothing ->
"no email"
-- Helper functions
emailOrDefault : String
emailOrDefault =
Maybe.withDefault "no email" email
Roc:
# Inline tag union (no built-in Maybe)
email : [Some Str, None]
email = Some("[email protected]")
# Pattern match
emailText : Str
emailText =
when email is
Some(addr) -> addr
None -> "no email"
# Manual helper or use Result
Translation:
Maybe a → [Some a, None]Just value → Some(value)Nothing → NoneElm:
-- Result error ok
divide : Int -> Int -> Result String Int
divide a b =
if b == 0 then
Err "Division by zero"
else
Ok (a // b)
Roc:
# Result ok err (REVERSED!)
divide : I64, I64 -> Result I64 Str
divide = \a, b ->
if b == 0 then
Err("Division by zero")
else
Ok(a // b)
CRITICAL:
Result error ok has error firstResult ok err has success firstElm:
classify : Int -> String
classify n =
case n of
0 ->
"zero"
x ->
if x < 0 then
"negative"
else
"positive"
Roc:
classify : I64 -> Str
classify = \n ->
when n is
0 -> "zero"
x if x < 0 -> "negative"
_ -> "positive"
Why this translation:
case becomes Roc's whenif expressions in branches, Roc has guard clauses (if after pattern)Elm:
doubled : List Int
doubled =
List.map (\x -> x * 2) [ 1, 2, 3, 4, 5 ]
-- Pipeline style
result : Int
result =
[ 1, 2, 3, 4, 5 ]
|> List.map (\x -> x * 2)
|> List.filter (\x -> x > 5)
|> List.foldl (+) 0
Roc:
doubled : List I64
doubled = List.map([1, 2, 3, 4, 5], \x -> x * 2)
# Pipeline style (same!)
result : I64
result = [1, 2, 3, 4, 5]
|> List.map(\x -> x * 2)
|> List.keepIf(\x -> x > 5)
|> List.walk(0, Num.add)
Why this translation:
List.filter → List.keepIf (different name, same semantics)List.foldl / List.foldr → List.walk (different name)|> is identicalList.map(list, fn) and List.map list fn syntaxElm:
user =
{ name = "Alice", age = 30 }
olderUser =
{ user | age = 31 }
-- Multiple fields
updatedUser =
{ user
| age = 31
, name = "Alice Smith"
}
Roc:
user = { name: "Alice", age: 30 }
olderUser = { user & age: 31 }
# Multiple fields
updatedUser = { user &
age: 31,
name: "Alice Smith"
}
Why this translation:
| for updates, Roc uses &= for field assignment, Roc uses :Elm:
type Msg
= GotData (Result Http.Error String)
fetchData : Cmd Msg
fetchData =
Http.get
{ url = "https://api.example.com/data"
, expect = Http.expectString GotData
}
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FetchData ->
( model, fetchData )
GotData result ->
case result of
Ok data ->
( { model | data = String.toUpper data }
, Cmd.none
)
Err _ ->
( { model | error = Just "Failed" }
, Cmd.none
)
Roc:
main : Task {} []
main =
data = Http.get!("https://api.example.com/data")
processed = Str.toUpper(data)
Task.ok({})
Why this translation:
Task.perform becomes Roc's ! operatorElm:
calculate : Int -> Int -> Int -> Result String Int
calculate a b c =
divide a b
|> Result.andThen (\x -> divide x c)
-- Or with explicit pattern matching
calculateExplicit : Int -> Int -> Int -> Result String Int
calculateExplicit a b c =
case divide a b of
Err e ->
Err e
Ok x ->
case divide x c of
Err e ->
Err e
Ok y ->
Ok y
Roc:
# Roc: Result ok err (reversed params!)
calculate : I64, I64, I64 -> Result I64 Str
calculate = \a, b, c ->
x = divide!(a, b) # Returns early on Err
y = divide!(x, c) # Returns early on Err
Ok(y)
Why this translation:
Result.andThen becomes Roc's ! operator! provides automatic early return on errorElm:
-- Elm uses module visibility for opacity
module Age exposing (Age, create, toInt)
type Age
= Age Int
create : Int -> Maybe Age
create n =
if n >= 0 && n < 150 then
Just (Age n)
else
Nothing
toInt : Age -> Int
toInt (Age n) =
n
-- Constructor Age is NOT exposed, only create function
Roc:
interface Age
exposes [Age, create, toU32]
imports []
# Opaque type
Age := U32
create : U32 -> Result Age [InvalidAge]
create = \n ->
if n >= 0 && n < 150 then
Ok(@Age(n))
else
Err(InvalidAge)
toU32 : Age -> U32
toU32 = \@Age(n) -> n
Why this translation:
@ syntax@Age(n) wrapping is more explicit than Elm's Age nResult for validation, Elm uses Maybe (different conventions)| Elm Concept | Roc Approach | Key Insight |
|---|---|---|
| Model-Update-View loop | Task chain (sequential) | Declarative → Imperative |
| Browser provides events | Platform provides I/O | Browser → CLI/Native |
main returns Program | main returns Task | Pure → Effect |
| Cmd issued, Msg received | Tasks compose with ! | Indirect → Direct |
| Elm Model | Roc Model | Conceptual Translation |
|---|---|---|
| Runtime handles Cmd/Sub | Platform handles Tasks | Both managed by runtime |
| Asynchronous with Msg | Sequential with ! | Event-driven → Chain |
| Cmd.none / new Cmd | Task.ok/Task.err | Side effect → Return value |
Elm (TEA):
type alias Model =
{ users : RemoteData Http.Error (List User)
}
type Msg
= FetchUsers
| GotUsers (Result Http.Error (List User))
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FetchUsers ->
( { model | users = Loading }
, Http.get
{ url = "/api/users"
, expect = Http.expectJson GotUsers usersDecoder
}
)
GotUsers result ->
case result of
Ok users ->
( { model | users = Success users }, Cmd.none )
Err error ->
( { model | users = Failure error }, Cmd.none )
Roc (Platform):
main : Task {} []
main =
users = Http.get!("/api/users")
decoded = Decode.fromBytes!(users, usersDecoder)
Stdout.line!("Fetched \(List.len(decoded) |> Num.toStr) users")
Key differences:
Key Difference: Parameter order is reversed!
-- Elm: Result err ok
parseAge : String -> Result String Int
parseAge str =
case String.toInt str of
Just age ->
if age >= 0 then
Ok age
else
Err "Negative age"
Nothing ->
Err "Not a number"
# Roc: Result ok err (REVERSED!)
parseAge : Str -> Result U32 Str
parseAge = \str ->
when Str.toU32(str) is
Ok(age) if age >= 0 -> Ok(age)
Ok(_) -> Err("Negative age")
Err(_) -> Err("Not a number")
Elm uses named custom types:
type FetchError
= NetworkError Http.Error
| NotFound
| Unauthorized
fetchUser : Int -> Task FetchError User
Roc uses inline tag unions:
fetchUser : U64 -> Task User [NetworkError, NotFound, Unauthorized]
Translation:
Elm Cmd (event-driven):
type Msg
= GotData (Result Http.Error String)
fetchData : Cmd Msg
fetchData =
Http.get
{ url = "https://api.example.com/data"
, expect = Http.expectString GotData
}
-- Must handle result in update function
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotData result ->
-- Handle result here
...
Roc Task (sequential):
fetchData : Task Str []
fetchData =
Http.get!("https://api.example.com/data")
Why this translation:
!Elm Subscriptions:
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Time.every 1000 Tick
, Browser.Events.onResize WindowResized
]
Roc approach: Roc doesn't have built-in subscriptions. Platforms may provide equivalent mechanisms through Task-based polling or event streams, but this is platform-specific.
For periodic tasks, you'd typically use platform-specific APIs or structure your main Task to loop.
Elm:
module User exposing (User, create, getName, getAge)
type alias User =
{ name : String
, age : Int
}
create : String -> Int -> User
create name age =
{ name = name, age = age }
getName : User -> String
getName user =
user.name
getAge : User -> Int
getAge user =
user.age
Roc:
interface User
exposes [User, create, getName, getAge]
imports []
User : {
name : Str,
age : U32,
}
create : Str, U32 -> User
create = \name, age ->
{ name, age }
getName : User -> Str
getName = \user -> user.name
getAge : User -> U32
getAge = \user -> user.age
Translation:
module → interfaceexposing → exposestype alias → type annotationElm:
import Dict
import Dict exposing (Dict)
import List exposing (map, filter)
import Maybe exposing (Maybe(..))
import Html as H
import Html.Events as Events
Roc:
import Dict
import Dict exposing [Dict]
import List exposing [map, keepIf]
import pf.Stdout
import pf.Task exposing [Task]
# Note: Roc doesn't have import aliasing yet
# Must use full qualified names
Translation:
exposing → exposing() → Brackets []import as → Not yet available in RocResult parameter order reversal (MOST CRITICAL)
Result err okResult ok errRecord update syntax
{ record | field = value }{ record & field: value }|/& and =/:Case vs When syntax
case x ofwhen x isis not ofPlatform target mismatch
Bang operator vs explicit Task
! operator, use Task.perform or Cmdvalue = task! for sequential executionCapitalization
True, FalseBool.true, Bool.falseFunction types
a -> b -> c (curried)a, b -> c (comma-separated params)Maybe vs tag union
Maybe a with Just/Nothing[Some a, None] (no built-in Maybe)List function names
List.filter, List.foldl, List.foldrList.keepIf, List.walkString interpolation
"Hello, " ++ name ++ "!""Hello, \(name)!"| Tool | Elm | Roc | Notes |
|---|---|---|---|
| Formatter | elm-format | roc format | Both enforce standard style |
| REPL | elm repl | roc repl | Both support interactive testing |
| Test | elm-test | roc test | Different syntax (case vs expect) |
| Build | elm make | roc build | Elm → JavaScript, Roc → native |
| Package manager | elm install | Platform URLs | Roc uses URL-based dependencies |
| Linter | elm-review | N/A | Elm has rich linting, Roc doesn't yet |
Before (Elm):
type alias User =
{ name : String
, age : Int
}
greet : User -> String
greet user =
"Hello, " ++ user.name ++ "! You are " ++ String.fromInt user.age ++ " years old."
-- Test
import Test exposing (test)
import Expect
suite =
test "greet formats message correctly" <|
\_ ->
greet { name = "Alice", age = 30 }
|> Expect.equal "Hello, Alice! You are 30 years old."
After (Roc):
User : { name : Str, age : U32 }
greet : User -> Str
greet = \user ->
"Hello, \(user.name)! You are \(Num.toStr(user.age)) years old."
expect greet({ name: "Alice", age: 30 }) == "Hello, Alice! You are 30 years old."
Key changes:
String → Str, Int → U32++ → interpolation \(...)String.fromInt → Num.toStrexpecttype alias → type annotationBefore (Elm):
type Color
= Red
| Green
| Blue
| Custom Int Int Int
toHex : Color -> String
toHex color =
case color of
Red ->
"#FF0000"
Green ->
"#00FF00"
Blue ->
"#0000FF"
Custom r g b ->
"#" ++ toHexByte r ++ toHexByte g ++ toHexByte b
toHexByte : Int -> String
toHexByte n =
-- Implementation using Hex library
String.fromInt n -- Simplified
After (Roc):
Color : [Red, Green, Blue, Custom(U8, U8, U8)]
toHex : Color -> Str
toHex = \color ->
when color is
Red -> "#FF0000"
Green -> "#00FF00"
Blue -> "#0000FF"
Custom(r, g, b) ->
"#\(toHexByte(r))\(toHexByte(g))\(toHexByte(b))"
toHexByte : U8 -> Str
toHexByte = \n ->
# Implementation
Num.toStr(n) # Simplified
Key changes:
type declaration → Structural tag unioncase x of → when x isInt → U8 (Roc has sized integers)Custom r g b → Custom(r, g, b)Before (Elm):
module Main exposing (main)
import Browser
import Html exposing (Html, div, text, button)
import Html.Events exposing (onClick)
import Http
import Json.Decode as Decode exposing (Decoder)
-- MODEL
type alias User =
{ id : Int
, name : String
, email : String
}
type RemoteData e a
= NotAsked
| Loading
| Success a
| Failure e
type alias Model =
{ user : RemoteData Http.Error User
}
init : () -> ( Model, Cmd Msg )
init _ =
( { user = NotAsked }, Cmd.none )
-- UPDATE
type Msg
= FetchUser
| GotUser (Result Http.Error User)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FetchUser ->
( { model | user = Loading }
, fetchUser 1
)
GotUser result ->
case result of
Ok user ->
( { model | user = Success user }
, Cmd.none
)
Err error ->
( { model | user = Failure error }
, Cmd.none
)
-- HTTP
fetchUser : Int -> Cmd Msg
fetchUser userId =
Http.get
{ url = "https://api.example.com/users/" ++ String.fromInt userId
, expect = Http.expectJson GotUser userDecoder
}
userDecoder : Decoder User
userDecoder =
Decode.map3 User
(Decode.field "id" Decode.int)
(Decode.field "name" Decode.string)
(Decode.field "email" Decode.string)
-- VIEW
view : Model -> Html Msg
view model =
div []
[ case model.user of
NotAsked ->
button [ onClick FetchUser ] [ text "Fetch User" ]
Loading ->
text "Loading..."
Success user ->
div []
[ text ("User: " ++ user.name ++ " (" ++ user.email ++ ")")
]
Failure error ->
text ("Error: " ++ httpErrorToString error)
]
httpErrorToString : Http.Error -> String
httpErrorToString error =
case error of
Http.BadUrl url ->
"Bad URL: " ++ url
Http.Timeout ->
"Timeout"
Http.NetworkError ->
"Network error"
Http.BadStatus status ->
"Bad status: " ++ String.fromInt status
Http.BadBody body ->
"Bad body: " ++ body
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
After (Roc):
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br"
}
import pf.Http
import pf.Stdout
import pf.Task exposing [Task]
import json.Decode
# Note: Result type parameters REVERSED!
# Elm: Result Http.Error User
# Roc: Result User [HttpErr]
User : { id : U64, name : Str, email : Str }
fetchUser : U64 -> Task User [HttpErr, DecodeErr]
fetchUser = \userId ->
url = "https://api.example.com/users/\(Num.toStr(userId))"
response = Http.get!(url)
when Decode.fromBytes(response.body, userDecoder) is
Ok(user) -> Task.ok(user)
Err(err) -> Task.err(DecodeErr)
userDecoder : Decode.Decoder User
userDecoder =
Decode.record(\field ->
{
id: field.required("id", Decode.u64),
name: field.required("name", Decode.str),
email: field.required("email", Decode.str),
}
)
main : Task {} []
main =
when fetchUser(1) is
Ok(user) ->
Stdout.line!("User: \(user.name) (\(user.email))")
Err(HttpErr) ->
Stdout.line!("HTTP error occurred")
Err(DecodeErr) ->
Stdout.line!("Failed to decode user")
Key changes:
Cmd Msg handling → Roc's ! operatorResult Http.Error User → Roc's Result User [HttpErr, DecodeErr] (reversed params!)Elm:
-- tests/UserTests.elm
module UserTests exposing (suite)
import Test exposing (Test, describe, test)
import Expect
import User
suite : Test
suite =
describe "User module"
[ describe "greet"
[ test "formats greeting correctly" <|
\_ ->
User.greet { name = "Alice", age = 30 }
|> Expect.equal "Hello, Alice! You are 30 years old."
, test "handles young age" <|
\_ ->
User.greet { name = "Bob", age = 5 }
|> Expect.equal "Hello, Bob! You are 5 years old."
]
]
Roc:
# User.roc
interface User
exposes [User, greet]
imports []
User : { name : Str, age : U32 }
greet : User -> Str
greet = \user ->
"Hello, \(user.name)! You are \(Num.toStr(user.age)) years old."
# Inline tests
expect greet({ name: "Alice", age: 30 }) == "Hello, Alice! You are 30 years old."
expect greet({ name: "Bob", age: 5 }) == "Hello, Bob! You are 5 years old."
Translation:
expectdescribe and test → Roc's flat expect statementsExpect.equal → Roc's == operatorelm-test → Run with roc testFor more examples and patterns, see:
meta-convert-dev - Foundational patterns with cross-language examplesconvert-roc-elm - Reverse conversion (Roc → Elm)convert-elm-haskell - Similar functional language conversion patternslang-elm-dev - Elm development patternslang-roc-dev - Roc development patternsCross-cutting pattern skills:
patterns-concurrency-dev - Cmd/Sub vs Task comparisonpatterns-serialization-dev - JSON encoding/decoding across languagespatterns-metaprogramming-dev - Why both languages avoid metaprogrammingxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience